一、多线程用途
-
提高运行效率(多核设备情况下),一般来说单核设备下的多线程属于假的多线程,但是在多核情况下,多线程能大大提高效率,充分的利用cpu
-
防止阻塞 多线程能异步处理一些耗时任务,防止阻塞,比如要请求其他服务获取一些数据,但是后续有些东西又不是依赖返回的数据,所以这里可以使用多线程进行异步处理。
二、多线程创建
-
继承Thread类
-
实现Runnable接口
public class testThread implements Runnable{
@Override
public void run() {
System.out.println("testThread run");
}
public static void main(String[] args) {
testThread thread = new testThread();
Thread t = new Thread(thread);
t.start();
}
}
- 实现Callable 接口
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class testThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//计算逻辑
return 1;
}
public static void main(String[] args) {
testThread td = new testThread();
FutureTask<Integer> result = new FutureTask<>(td);
new Thread(result).start();
try {
Integer sum = result.get(); //FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
System.out.println(sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
关于 run和 start方法。start方法是启动一个线程的方法,而线程中的run方法是线程中需要执行的东西。如果直接调用run方法,会当作同步使用,所以这里一定要注意,多线程启动的方法是start 不是run。
- ExecutorService 线程池启动,和者没有本质区别,只是在启动方式上有区别
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class TestThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//计算逻辑
return 1;
}
public static void main(String[] args) {
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
//也可以用 runable接口
pool.submit(new TestThread1());
for (int i = 0; i < taskSize-1; i++) {
Callable c = new TestThread();
// 执行任务并获取Future对象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
try {
System.out.println(">>>" + f.get().toString());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
还有通过匿名方法创建多线程并启动,这里不做多描述
三、关键字Volatile 和 synchronized
volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。 一般来说,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰,会产生下列影响:
1 线程间操作的可见性
这里要稍微提一下内存可见性
public class SynchronizedTest{
boolean status = false;
/**
* 状态切换为true
*/
public void changeStatus(){
status = true;
}
/**
* 若状态为true,则running。
*/
public void run(){
if(status){
System.out.println("running....");
}
}
}
如上述代码,假设在一个线程中执行了 changestatus方法,另一个线程中并不一定能打印出想要的结果,原因在于可见性。 所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述的例子中是没有办法做到内存可见性的。 JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下
所以对于上面的代码只需将status变量加上 volatile 关键字。
但是这个关键字并不是万能的。在一些复合类操作中,会存在一些问题
mport java.util.concurrent.CountDownLatch;
public class SynchronizedTest{
public static volatile int num = 0;
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操作
for(int i=0;i<30;i++){
new Thread(){
public void run(){
for(int j=0;j<10000;j++){
num++;//自加操作 num = num+1;也是同理
//int i= num;num = i+1; 同理
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num);
}
}
输出: 268270
按照之前说的 使用 volatile 修饰的,理应输出 300000。这里问题就出在一个操作 num++,num++不是原子性的操作, 可以将这一步理解为 三部操作, 读取 加一 赋值,但是 这里的 读取和 加一都是在本地内存中,所以可能其他线程的+1操作已经执行了很多了,然后这里又会进行相应的覆盖,所以结果要小于预期结果。所以在java并发包中提供了一个方案针对这个操作的
//使用原子操作类
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操作
for(int i=0;i<30;i++){
new Thread(){
public void run(){
for(int j=0;j<10000;j++){
num.incrementAndGet();//原子性的num++,通过循环CAS方式
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num);
}
结果: 300000
2:volatile能禁止指令重排序
一般来说,java虚拟机为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,会按照自己的规则在语义上不影响对程序的编写顺序进行一些打乱,如下面的例子:
public class SynchronizedTest extends Thread{
/** 这是一个验证结果的变量 */
private static int a=1;
/** 这是一个标志位 */
private static boolean flag=false;
//由于多线程情况下未必会试出重排序的结论,所以多试一些次
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
ThreadA threadA=new ThreadA();
ThreadB threadB=new ThreadB();
threadA.start();
threadB.start();
//这里等待线程结束后,重置共享变量,以使验证结果的工作变得简单些.
threadA.join();
threadB.join();
a=0;
flag=false;
}
}
static class ThreadA extends Thread{
public void run(){
a=1;
flag=true;
}
}
static class ThreadB extends Thread{
public void run(){
if(flag){
a=a*1;
}
if(a==0){
System.out.println("ha,a==0");
}
}
}
}
打印结果:ha,a==0 或者 无打印
这里就有疑问了 a=0的赋值是在线程执行完之后,为什么还会出现 a=0的情况呢?原因就在与虚拟机的重排序,按照main方法里面的逻辑,都没有直接的对 a变量进行读写操作(没有对这个变量有依赖),所以可能对这个赋值的指令进行重排序。(并非一定出现,可以多运行几次) 但是使用 volatile 就不一样了,指明了关于这个变量相关的指令不进行重排序。
volatile原理如下: “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令” lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
下面说下synchronized关键字:
synchronized也是多线程开发中一个比较重要的关键字, 可用于修饰代码块和方法,
public class SynchronizedTest1 extends Thread{
private SynchronizedTest synchronizedTest;
private boolean flag;
SynchronizedTest1(SynchronizedTest synchronizedTest, boolean flag){
this.synchronizedTest = synchronizedTest;
this.flag = flag;
}
@Override
public void run() {
if (flag){
try {
synchronizedTest.methodA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
synchronizedTest.methodB();
}
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
SynchronizedTest1 synchronizedTest1 = new SynchronizedTest1(synchronizedTest, true);
SynchronizedTest1 synchronizedTest2 = new SynchronizedTest1(synchronizedTest, false);
synchronizedTest1.start();
synchronizedTest2.start();
}
}
public class SynchronizedTest{
// 修饰方法 如果方法是static 则锁定该类所有实例
synchronized public void methodA() throws InterruptedException {
//do something....
System.out.println("methodA");
Thread.sleep(10000);
}
public void methodB() {
// 修饰代码块
synchronized (this) {
//do something....
System.out.println("methodB");
}
}
}
打印结果:
methodAThu Mar 07 11:27:33 CST 2019
methodBThu Mar 07 11:27:43 CST 2019
从上述例子可以看出,当synchronized修饰时,若多个线程拥有同一个MyObject类的对象,则这些方法只能以同步的方式执行。即,执行完一个synchronized修饰的方法或代码块后,才能执行另一个synchronized修饰的方法或代码块。(可以理解为锁)
3 关键词使用范例
(1)synchronized, wait, notify结合:典型场景生产者消费者问题
public class Tip1 {
private int product;
private static int MAX_PRODUCT = 10;
private static int MIN_PRODUCT = 1;
/**
* 生产者生产出来的产品交给店员
*/
public synchronized void produce()
{
if(this.product >= MAX_PRODUCT)
{
try
{
wait();
System.out.println("产品已满,请稍候再生产");
}
catch(InterruptedException e)
{
e.printStackTrace();
}
return;
}
this.product++;
System.out.println("生产者生产第" + this.product + "个产品.");
notifyAll(); //通知等待区的消费者可以取出产品了
}
/**
* 消费者从店员取产品
*/
public synchronized void consume()
{
if(this.product <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺货,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消费者取走了第" + this.product + "个产品.");
this.product--;
notifyAll(); //通知等待去的生产者可以生产产品了
}
}
(2) 单例创建
import java.util.Objects;
public class TestIns {
private volatile static TestIns testIns;
public static TestIns getTestIns(){
if (Objects.isNull(testIns)) {
synchronized (TestIns.class) {
}
}
return testIns;
}
}
需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。testIns = new TestIns();可以分解为3行伪代码 1.memory = allocate() //分配内存 2. ctorInstanc(memory) //初始化对象 3. testIns = memory //设置testIns指向刚分配的地址 上面的代码在编译运行时,可能会出现重排序从1-2-3排序为1-3-2。在多线程的情况下会出现以下问题。线程A在执行第5行代码时,B线程进来,而此时A执行了1和3,没有执行2,此时B线程判断instance不为null,直接返回一个未初始化的对象。
四、多线程相关方法
- thread 相关
//当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
public static Thread.yield()
//暂停一段时间
public static Thread.sleep()
//在一个线程中调用other.join(),将等待other执行完后才继续本线程。    
public join()
//后两个函数皆可以被打断
public interrupte()
关于中断
它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。 Thread.interrupted()检查当前线程是否发生中断,返回boolean synchronized在获锁的过程中是不能被中断的。
中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体
- Callable相关
future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态
ExecutorService e = Executors.newFixedThreadPool(3);
//submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 无阻塞
future.get() // return 返回值,阻塞直到该线程运行结束
- ThreadLocal
用处:保存线程的独立变量。对一个线程类(继承自Thread) 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。
实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。 主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。
log4j的xml能直接读到 %X{参数名} 常用来自定义日志,微服务打印日志,做日志集中处理的时候会用到
- AtomicInteger和AtomicBoolean AtomicReference原子类
//返回值为boolean
AtomicInteger.compareAndSet(int expect,int update)
对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。 这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号
- Lock相关
// 共有三个实现类
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。
lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序) 提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。 本质上和监视器锁(即synchronized是一样的) 能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。 和Condition类的结合。 性能更高,对比如下图:
基本的线程相关的就这些了,还有些更加深入的以后继续补充
如何让多线程顺序执行 join方法
static ExecutorService executorService = Executors.newSingleThreadExecutor();