老师发糖了,要排排队领取哦!Java synchronized关键字

这个多人排队领取东西的场景和编程中的多线程访问共享资源的场景很像 。 今天我们结合Javasynchronized关键字来讲解下 。
1、什么时候会出现线程安全问题?在单线程中不会出现线程安全问题 , 而在多线程编程中 , 有可能会出现同时访问同一个资源的情况 , 这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等 , 而当多个线程同时访问同一个资源的时候 , 就会存在一个问题:
由于每个线程执行的过程是不可控的 , 所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错 。
举个简单的例子:
现在有两个线程分别从网络上读取数据 , 然后插入一张数据库表中 , 要求不能插入重复的数据 。
那么必然在插入数据的过程中存在两个操作:
1)检查数据库中是否存在该条数据;
【老师发糖了,要排排队领取哦!Java synchronized关键字】2)如果存在 , 则不插入;如果不存在 , 则插入到数据库中 。
假如两个线程分别用thread-1和thread-2表示 , 某一时刻 , thread-1和thread-2都读取到了数据X , 那么可能会发生这种情况:
thread-1去检查数据库中是否存在数据X , 然后thread-2也接着去检查数据库中是否存在数据X 。
结果两个线程检查的结果都是数据库中不存在数据X , 那么两个线程都分别将数据X插入数据库表当中 。
这个就是线程安全问题 , 即多个线程同时访问一个资源时 , 会导致程序运行结果并不是想看到的结果 。
这里面 , 这个资源被称为:临界资源(也有称为共享资源) 。
也就是说 , 当多个线程同时访问临界资源(一个对象 , 对象中的属性 , 一个文件 , 一个数据库等)时 , 就可能会产生线程安全问题 。
不过 , 当多个线程执行一个方法 , 方法内部的局部变量并不是临界资源 , 因为方法是在栈上执行的 , 而Java栈是线程私有的 , 因此不会产生线程安全问题 。
2、如何解决线程安全问题?基本上所有的并发模式在解决线程安全问题时 , 都采用“序列化访问临界资源”的方案 , 即在同一时刻 , 只能有一个线程访问临界资源 , 也称作同步互斥访问 。
通常来说 , 是在访问临界资源的代码前面加上一个锁 , 当访问完临界资源后释放锁 , 让其他线程继续访问 。
在Java中 , 提供了两种方式来实现同步互斥访问:synchronized和Lock 。
本文主要讲述synchronized的使用方法 , Lock的使用方法后续再讲 。
3、synchronized同步方法、同步块我们先来看一个概念:互斥锁 , 顾名思义:能到达到互斥访问目的的锁 。
举个简单的例子:如果对临界资源加上互斥锁 , 当一个线程在访问该临界资源时 , 其他线程便只能等待 。
在Java中 , 每一个对象都拥有一个锁标记(monitor) , 也称为监视器 , 多线程同时访问某个对象时 , 线程只有获取了该对象的锁才能访问 。
在Java中 , 可以使用synchronized关键字来标记一个方法或者代码块 , 当某个线程调用该对象的synchronized方法或者访问synchronized代码块时 , 这个线程便获得了该对象的锁 , 其他线程暂时无法访问这个方法 , 只有等待这个方法执行完毕或者代码块执行完毕 , 这个线程才会释放该对象的锁 , 其他线程才能执行这个方法或者代码块 。
下面通过几个简单的例子来说明synchronized关键字的使用:
①synchronized方法
下面这段代码中两个线程分别调用insertData对象插入数据:
publicclassTest{publicstaticvoidmain(String[]args){finalInsertDatainsertData=https://pcff.toutiao.jxnews.com.cn/p/20200810/newInsertData();newThread(){publicvoidrun(){insertData.insert(Thread.currentThread());};}.start();newThread(){publicvoidrun(){insertData.insert(Thread.currentThread());};}.start();}}classInsertData{privateListlist=newArrayList();publicvoidinsert(Threadthread){for(inti=0;i<5;i++){System.out.println("线程"+thread.getName()+"在插入数据"+i);list.add(i);}}}输出结果可以看出:两个线程在同时执行insert方法 , 不存在相互等待的情况 。
线程Thread-0在插入数据0线程Thread-1在插入数据0线程Thread-0在插入数据1线程Thread-1在插入数据1线程Thread-0在插入数据2线程Thread-0在插入数据3线程Thread-1在插入数据2线程Thread-0在插入数据4线程Thread-1在插入数据3线程Thread-1在插入数据4下面我们在insert方法前面加上关键字synchronized:
classInsertData{privateListlist=newArrayList();publicsynchronizedvoidinsert(Threadthread){for(inti=0;i<5;i++){System.out.println("线程"+thread.getName()+"在插入数据"+i);list.add(i);}}}输出结果可以看出:Thread-1插入数据是等Thread-0插入完数据之后才进行的 。 说明Thread-0和Thread-1是顺序执行insert方法的 。
线程Thread-0在插入数据0线程Thread-0在插入数据1线程Thread-0在插入数据2线程Thread-0在插入数据3线程Thread-0在插入数据4线程Thread-1在插入数据0线程Thread-1在插入数据1线程Thread-1在插入数据2线程Thread-1在插入数据3线程Thread-1在插入数据4注意:
1)当一个线程正在访问一个对象的synchronized方法 , 那么其他线程不能访问该对象的其他synchronized方法 。 这个原因很简单 , 因为一个对象只有一把锁 , 当一个线程获取了该对象的锁之后 , 其他线程无法获取该对象的锁 , 所以无法访问该对象的其他synchronized方法 。
2)当一个线程正在访问一个对象的synchronized方法 , 那么其他线程能访问该对象的非synchronized方法 。 这个原因很简单 , 访问非synchronized方法不需要获得该对象的锁 , 假如一个方法没用synchronized关键字修饰 , 说明它不会使用到临界资源 , 那么其他线程是可以访问这个方法的 ,
3)如果一个线程A需要访问对象object1的synchronized方法fun1 , 另外一个线程B需要访问对象object2的synchronized方法fun1 , 即使object1和object2是同一类型) , 也不会产生线程安全问题 , 因为他们访问的是不同的对象 , 所以不存在互斥问题 。
②synchronized代码块
synchronized代码块类似于以下这种形式:
synchronized(synObject){}当在某个线程中执行这段代码块 , 该线程会获取对象synObject的锁 , 从而使得其他线程无法同时访问该代码块 。
synObject可以是this , 代表获取当前对象的锁 , 也可以是类中的一个属性 , 代表获取该属性的锁 。
比如上面的insert方法可以改成以下两种形式:
classInsertData{privateListlist=newArrayList();publicvoidinsert(Threadthread){synchronized(this){for(inti=0;i<5;i++){System.out.println("线程"+thread.getName()+"在插入数据"+i);list.add(i);}}}}classInsertData{privateListlist=newArrayList();privateObjectobject=newObject();publicvoidinsert(Threadthread){synchronized(object){for(inti=0;i<5;i++){System.out.println("线程"+thread.getName()+"在插入数据"+i);list.add(i);}}}}从上面可以看出 , synchronized代码块使用起来比synchronized方法要灵活得多 。 因为也许一个方法中只有一部分代码只需要同步 , 如果此时对整个方法用synchronized进行同步 , 会影响程序执行效率 。 而使用synchronized代码块就可以避免这个问题 , synchronized代码块可以实现只对需要同步的地方进行同步 。
另外 , 每个类也会有一个锁 , 它可以用来控制对static数据成员的并发访问 。
并且如果一个线程执行一个对象的非staticsynchronized方法 , 另外一个线程需要执行这个对象所属类的staticsynchronized方法 , 此时不会发生互斥现象 , 因为访问staticsynchronized方法占用的是类锁 , 而访问非staticsynchronized方法占用的是对象锁 , 所以不存在互斥现象 。
看下面这段代码就明白了:
publicclassTest{publicstaticvoidmain(String[]args){finalInsertDatainsertData=https://pcff.toutiao.jxnews.com.cn/p/20200810/newInsertData();newThread(){@Overridepublicvoidrun(){insertData.insert(Thread.currentThread());}}.start();newThread(){@Overridepublicvoidrun(){insertData.insert1(Thread.currentThread());}}.start();}}classInsertData{publicsynchronizedvoidinsert(Threadt){System.out.println("线程"+t.getName()+"执行insert开始");try{Thread.sleep(5000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("线程"+t.getName()+"执行insert结束");}publicsynchronizedstaticvoidinsert1(Threadt){System.out.println("线程"+t.getName()+"执行insert1开始");System.out.println("线程"+t.getName()+"执行insert1结束");}}输出结果可以看出:第一个线程里面执行的是insert方法 , 不会导致第二个线程执行insert1方法发生阻塞现象 。 两个线程用的不是同一把锁 , 所以不会出现同步阻塞现象 。
线程Thread-0执行insert开始线程Thread-1执行insert1开始线程Thread-1执行insert1结束线程Thread-0执行insert结束下面我们看一下synchronized关键字到底做了什么事情 , 我们来反编译它的字节码看一下 , 下面这段代码反编译后的字节码为:
packagecom.testsyn;publicclassInsertData{privateObjectobject=newObject();publicvoidinsert1(Threadthread){//属性对象锁synchronized(object){}}publicvoidinsert2(Threadthread){//当前对象锁synchronized(this){}}publicvoidinsert3(Threadthread){//类锁synchronized(InsertData.class){}}//static方法锁publicstaticsynchronizedvoidinsert4(Threadthread){}//非static方法锁publicsynchronizedvoidinsert5(Threadthread){}//没有锁publicvoidinsert6(Threadthread){}}对生成的字节码文件执行命令javap-vInsertData.class , 结果如下:
注意:对于synchronized方法或者synchronized代码块 , 当出现异常时 , JVM会自动释放当前线程占用的锁 , 因此不会由于异常导致出现死锁现象 。
欢迎小伙伴们留言交流~


    推荐阅读