synchronized关键字(一)


一.并发编程的三大问题

一.可见性

 1.概念:

 对于共享数据,一个线程对该数据的值进行修改后另一个线程无法得到修改后的值

 2.代码演示:

public class Test1 {
private static boolean flag=true;
public static void main(String[] args) throws InterruptedException {
    new Thread(){
        @Override
        public void run() {
            while (flag){
                ;
            }
            System.out.println("flag已修改");
        }
    }.start();
    Thread.sleep(2000);
    new Thread(){
        @Override
        public void run() {
            flag=false;
            System.out.println("已经被我修改");
            }
        }.start();
    }
}  

运行结果:
不难发现,当线程二将flag改为true的时候线程一并不能提前得到最新数据,导致程序依旧停留在while循环中

3.原因分析


内存模型
 线程工作原理:在线程操作变量时,会先将主内存中的变量拷贝到工作内存中,然后对工作内存中的拷贝变量进行赋值计算等操作,操作完之后再赋值给主内存

 分析:比如示例代码的上下两个线程分别表示线程1和线程2,线程一的工作内存复制的值是true,当线程2将flag改为false并且赋给主内存后,线程一并不会去读取主内存中的值,这也就导致了线程一卡在while循环无法出来

4.解决方法

 方式一:用volatile修饰共享变量
 原理:被volatile修饰的变量,当主内存中该变量被别的线程二改变时,会将工作内存中的变量全部作废,这样线程一要重新向主内存中去读取数据的值

给flag添加了volatile之后代码的运行结果:

  方式二:用synchronized来保证可见性
仅就红色区域为新添的代码(synchronized的参数要放一个对象),也能解决这个问题

原理分析:
sychronized的时候会有lock和unlock的操作,在lock的时候会刷新当前线程工作内存中的值,实现工作内存及时读取最新的数据值

补充:正因为如此,所以如果仅仅只在while循环中加一句

 System.out.println("true");

最后的也能退出循环,因为println的源码中有用到synchronized

二.原子性

 1.概念:

 在一次或多次操作中,要么所有操作都执行,而且不会因为互相干扰出现异常结果,要么都不执行 (经典抢车票中的线程安全问题)

 2.代码演示(代码选自 创建多线程的四种方式中的方式二):

    //1.创建一个实现Runnable接口的类
    class Mythread implements Runnable{
    private int ticket=100;
    //2.重写run方法
    @Override
    public void run() {
        while (true){
            if(ticket>=1){
                System.out.println(Thread.currentThread().getName()+"抢到票"+ticket);
                ticket--;
            }
            else {
                  System.out.println(Thread.currentThread().getName()+"票已售罄");
                  break;
            }
        }
    }   
}
    public class Test1 {
        public static void main(String[] args) {
            //3.创建一个实现类的对象
            Mythread t= new Mythread();
            /*4.每个线程各自创建一个以实现类对象为参数的Thread  
            类的对象*/
            Thread t1=new Thread(t);
            Thread t2=new Thread(t);
            t1.setName("线程一");
            t2.setName("线程二");
            //5.start开启进程
            t1.start();
            t2.start();
        }
    }

运行结果:
不难发现,产生了重票

 3.原因分析:

 由于可能在同一时间多个线程同时对共享变量进行操作,正是由于同时操作,所以导致得到的票重复之类的问题

 4.解决方法

  方式一(syncronized):用synchronized进行同步的代码块,哪个进程拿到锁就哪个进程执行,如果没拿到的只能在外面等着,这保证了代码块最多只能有一个进程在执行

例如:对于上面的代码示例中while内部用synchronized包起来,这样当线程一拿到锁开始抢票的时候其他线程都在while那里等着不往下执行,等线程一执行完代码块后锁释放出来再和其他进程一起去抢

注意: synchronized如果是把while也包进来了结果会是只有一个进程直接把所有票抢完,原因可以结合上面的原理自行分析
所以使用synchronized有一个原则:不能包的太多也不能太少

三.有序性

 1.概念:

 代码执行顺序不是严格按照顺序从上到下执行的

 2.代码示例():

ready=true;
num=2;
经过重排序,执行顺序变成
num=2;
ready=true

此时,如果线程二先执行并且重排序的话可能导致r.r1=0,我们要做得就是避免这种情况为了避免这种情况

 3.原因分析

 程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序

 4.解决方法:

 方式一(synchronized关键字) :给上述代码的actor1和actor2内的代码加上synchronized,即便内部会发生重排序,但是不会影响到外部。因为加了synchronized之后只会有一个线程执行代码块中的内容
 方式二(volatile):给共享变量加上volatile,可以保证不发生重排序

二.synchronized的可重入性

 1.概念:

 synchronized允许多层嵌套

 2.代码示例:

class Mythread extends Thread{
//2.重写run方法
@Override
public void run() {
     synchronized (this){
         System.out.println(getName()+"进入同步代码块1");
         synchronized (this){
             System.out.println(getName()+"进入同步代码块2");
         }
     }
 }
}  
public class Test1 {
    public static void main(String[] args) {
        Mythread t1= new Mythread();
        Mythread t2= new Mythread();
        t1.setName("线程一");
        t2.setName("线程二");
        t1.start();
        t2.start();
    }
}

代码结果:

 3.可重入的原理,好处,小结


关于synchronized的计数器:每进入一层synchronized就+1,到了结尾就-1


三.synchronized的不可中断性

 1.概念:

 一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁, 第二个线程会一直阻塞或等待,不可被中断。

 1.代码示例:

class Mythread implements Runnable{
@Override
public void run() {
    synchronized (this){
    System.out.println(Thread.currentThread().getName()+"进入代码块");
        try {
            Thread.sleep(98899);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
}
public class Test1 {
public static void main(String[] args) {
    Mythread t= new Mythread();
    Thread t1=new Thread(t);
    Thread t2=new Thread(t);
    t1.setName("线程一");
    t2.setName("线程二");
    t1.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t2.start();
    System.out.println(t1.getState());
    System.out.println(t2.getState());
    }
}

代码结果:
当线程一还在执行的时候,线程二处于阻塞状态,即便手动暂停,线程二依旧处于阻塞状态,体现了synchronized的不可中断性,线程二只能在synchronized那一行等着锁释放

四.Reentrantlock的可中断和不可中断

 1.Lock可中断代码(lock.lock())

class Mythread implements Runnable{
Lock lock=new ReentrantLock();
@Override
public void run() {
    lock.lock();
    System.out.println(Thread.currentThread().getName()+"获得锁");
        try {
            sleep(50000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    System.out.println("释放锁");
    }
}
public class Test1 {
    public static void main(String[] args) {
        Mythread t= new Mythread();
        Thread t1=new Thread(t);
        Thread t2=new Thread(t);
        t1.setName("线程一");
        t2.setName("线程二");
        t1.start();
        t2.start();
        try {
            sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("停止第线程二前");
        t2.interrupt();
        System.out.println("停止第线程二后");
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

代码结果 :
结果显示 :线程二处于WAITING状态,不可暂停

 2.Lock的可中断性代码示例(trylock())

就是利用了 trylock,第二个参数是单位,比如SECOND就是秒


五.javap反汇编学习synchronized的原理

一.monitorenter

1.JVM规范文档和反汇编代码

JVM规范中描述monitorenter
左边是代码,右边是反编译的结果

 2.monitorenter小结

 synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待

二.monitorexit

 1.JVM规范文档和反汇编代码

JVM规范中描述monitorexit
左边是代码,右边是反编译的结果

注意:从反汇编的Exception table可以看出:synchronized一旦出现异常是会释放锁的

三.同步方法


也就是说在进入方法时隐式调用monitorenter,结束的时候隐式调用monitorexit

四.小结

  通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令.每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数当执行到monitorexit时,recursions会-1 ,当计数器减到0时这个线程就会释放锁

 五.synchronized和lock的区别(面试题)


注1:由于synchronized是重量锁,效率比较低,虽然相较于以前进行了优化
注2:ReentranLock是否是公平锁可以自己控制,在实例化lock的时候参数不写就默认非公平,(fair)表示公平锁

    ReentrantLock lock=new ReentrantLock();//非公平锁
    ReentrantLock lock=new ReentrantLock();//公平锁

公平锁 :即满足队列的原则,先进先出
非公平锁 :所有进程同时抢一把锁看运气


文章作者: 灿若星河
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 灿若星河 !
评论
 上一篇
JOL的安装 JOL的安装
下载 jol 工具包https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/选择一个版本,进去后下载 jol-cli-.-full.jar 一定要下载full 的jar 导
下一篇 
创建多线程的四种方式(暂时不考虑线程安全问题) 创建多线程的四种方式(暂时不考虑线程安全问题)
再次提醒:下文内容仅就总结多线程的创建方式,不考虑线程安全问题,且为了直观,在异常处理方面大量省略一.程序,进程,线程的基本概念 这里只要做简单的了解即可,红字部分有印象就行,详细内容在虚拟机部分做具体解释 二.创建多线程的四种方式(以多线
2020-07-19
  目录