1.JUC简介
在JDK1.5版本里,提供了java.util.concurrent(简称JUC)这个包,在此包中增加了许多有关于并发编程中常用的工具类。
2.基础概念
进程:进程是资源分配的最小单位(工厂)
线程:线程是CPU调度的最小单位(工人)
并发:CPU单核、多个线程操作同一资源
并行:CPU多核、多个线程同时执行
wait/sleep的区别:
来自不同的类:wait => Objectsleep => Thread关于锁的释放:wait会释放锁,而sleep不会
java创建多线程的三种方式:继承Thread类、实现Runnable接口、匿名内部类
3.最常见的锁:synchronized(经典的可重入锁)
synchronized的三种应用方式:
- 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
- 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
- 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

作用:Synchronized是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保线程互斥的访问同步代码
//修饰方法public synchronized void increase(){i++;}//代码块public class test{public void test(){synchronized(this){}}}
1、 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
2、每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3、实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制
synchronized是会自己升级的:偏向锁 → 轻量级锁 → 重量级锁
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。
经典面试题:手写一个单例模式
然后你心里想:简单,饿汉式、懒汉式都给你写一个
面试官:一般水平
那么如何展示我们的水平高呢,我们写一个双重校验锁的单例:
public class Singleton {//采用volatile修饰private volatile static Singleton singleton;//构造方法私有化private Singleton(){}//双重校验锁public static Singleton getInstance(){//先判断对象是否已经实例过,没有实例化过才进入加锁代码if(singleton == null){//类对象加锁synchronized(Singleton.class){//再次判断if (singleton == null){singleton = new Singleton();}}}return singleton;}}
使用volatile 的原因:避免jvm指令重排
因为 singleton = new Singleton() 这句话可以分为三步:
- 为 singleton 分配内存空间;
- 初始化 singleton;
- 将 singleton 指向分配的内存空间。
但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。
使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行
4.volatile关键字、内存可见性、原子性、有序性
Java 内存模型中的可见性、原子性和有序性。
内存可见性(Memory Visibility)
什么叫可见性?一个线程对共享变量值的修改,能够被其它线程看到
代码示例:
public class volatileTest {public static void main(String[] args) {ThreadDemo td = new ThreadDemo();new Thread(td).start();while(true){if(td.isFlag()){System.out.println("------------------");break;}}}}class ThreadDemo implements Runnable {private volatile boolean flag = false;@Overridepublic void run() {try {Thread.sleep(200);} catch (InterruptedException e) {}flag = true;System.out.println("flag=" + isFlag());}public boolean isFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}}
当我们去掉volatile关键字时:
我们可以发现,程序并未执行完成,说明了isFlag一直为false。
原因:
上面的程序中共有两个线程,一个是td ,一个是主线程,此时td线程修改了值,但是主线程就是没有获得到,这里可以根据上面的JMM得到答案,就是说此时有一块主存,线程td从主存中读取的flag=false,此时睡眠0.2秒,主线程从主存中也读取了flag=false。0.2秒过后,td线程中flag=true,但是main线程中while(true)执行的速度特别快,是计算机比较底层的代码,所以main线程一直都没有机会再次从主存中读取数据(此时他也不知道主存的数据被更改)。这两个线程之间操作共享数据彼此是不可见的。
volatile不具备原子性:
public class volatileTest2 {public static void main(String[] args) {myData myData=new myData();for (int i = 0; i <20 ; i++) {new Thread(()->{for (int j = 0; j <1000 ; j++) {myData.addPlusPlus();}},String.valueOf(i)).start();}while (Thread.activeCount()>2){ //java最起码有一个main线程,和一个垃圾回收线程,所以这里是2Thread.yield();//当前线程由执行态变为就绪态,让出cpu}System.out.println(myData.number);}}class myData{volatile int number=0;public void addPlusPlus(){this.number++;}}
我们运行三次看结果:
第一次:
第二次:
第三次:
这里我们得出一个结论 num++ 在多线程下是不安全的
因为num++ 实际上是分三步的,
第一步:栈中取出i
第二步:i自增1
第三步:将i存到栈
尽管用了volatile 第三步能够及时写入到内存。但是它不具备原子性,比如线程A从栈中取出i,此时完成了自增,发生了线程调度,此时线程B取出栈的值,尽管线程A里的值发生了更改,但是还未写到栈里,此时线程B操作的还是之前的值。这就证明了volatile不具备原子性。
如何具有原子性呢?
1、synchronized同步锁(不推荐、太重量级且效率低)
2、Atomic包(Compare And Swap(CAS))
public class TestVolatile1 {public static void main(String[] args) {myData myData=new myData();for (int i = 0; i <20 ; i++) {new Thread(()->{for (int j = 0; j <1000 ; j++) {myData.addAtomic();}},String.valueOf(i)).start();}while (Thread.activeCount()>2){ //java最起码有一个main线程,和一个垃圾回收线程Thread.yield();//当前线程由执行态变为就绪态,让出cpu}System.out.println(Thread.currentThread().getName()+"\t" +myData.atomicInteger);}}class myData{AtomicInteger atomicInteger=new AtomicInteger(); // 不用赋值,默认就是0public void addAtomic(){atomicInteger.getAndIncrement();// 表示i++}}
volatile禁止指令重排序
前提条件:计算机在执行程序的时候,为了提高性能,编译器和处理器会对指令进行重排序。
举例:
//线程一a = 1;flag = true;//线程二if(flag){a = a + 5;System.out.println(a);}
此时线程如果线程1的执行顺序a=1,flag=true,则线程2输出的结果为6,如果此时线程1排序后,先执行了flag=true,还未执行a=1,那么此时恰巧线程2获取了flag=true,最终结果就是5了。解决这个问题 变量前用volatile关键字就可以解决。
重排序对单线程无影响,只影响多线程。因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
参考文章:
