2018年拍摄于日本奈良的小鹿,鹿随便摸,手感棒棒的。
王皓的github:https://github.com/tenaciousdwang
上回说到线程的生命周期,今天接着说一下并发编程中肯定会遇到的线程安全问题,线程安全问题只出现在多线程环境,单线程串行环境下不会出现这样的问题,下面引用码出高效中的例子来说明一下。
医生坐诊,并发处理多个病人的询问、开化验单、查看化验结果、开药等工作,任何一个环节一旦出现数据混淆,都可能引发严重的医疗事故。
延伸到计算机的线程处理过程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:
由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。
再举一个例子,在服务端有一个账户,里面有10000元,有a线程执行用户查询,并进行扣款100元,有一个b线程执行同样的扣款100元操作,两个线程并发,无法保证执行顺序,a线程查询账户为10000元后交出cpu执行时间片,b线程开始执行也查询账户为10000元,交出cpu执行时间片后,a线程开始扣款10000元-100元写入9900元执行完毕,b线程开始扣款10000元-100元写入9900元执行完毕,这样账户实际扣款200元,但是最后数据却是9900元,而不是9800元,这就是线程安全问题。
为保证线程安全,在多个线程并发的竞争一个共享资源时,通常采用同步机制协调各个线程的执行,以确保得到正确的结果。
保证高并发场景下的线程安全,可以从以下四个维度来说:
1、数据单线程内可见,单线程内总是安全的,数据只在栈内可见,不共享,就不会被其他线程修改。
2、使用private final关键字使其只读,只读就不可修改,无法继承,总是安全的。
3、使用线程安全的类,比如stringbuffer,concurrenthashmap线程安全容器等。
4、同步与锁机制,在java中,提供了两种方式来实现同步互斥访问:synchronized和lock,线程的核心概念就是“要么只读,要么加锁”。
今天主要说一下synchronized关键字,同步代码块,在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁,即能到达到互斥访问目的的锁。
举一个简单的例子,还是我们上面所说的账户问题,如果我们给账户这个对象加上互斥锁,当一个线程进行访问时,另一个线程无法访问,必须等待第一个线程访问完并释放互斥锁后,再获取锁来访问,就可以避免刚才出现的扣款问题,这个时候当a线程执行完后,账户写入9900,接下来b线程获取锁进行访问,9900-100后写入9800元,为正常预期。
在java中,每一个对象都拥有一个锁标记(monitor),这是每个对象与生俱来的一个隐藏字段,也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。
synchronized锁的特性由jvm负责实现,jvm通过监视monitor来实现同步。方法元信息中会使用acc_synchronized识别该方法是否是一个同步方法。同时使用monitorenter与monitorexit来获取和释放monitor。
使用monitorenter进入时,monitor为0,表示该线程可以继续持有这个锁,并继续后续的代码,并将monitor加1,如果这个线程再次访问则monitor再加1,如果该对象monitor不是0,则其他访问这个对象的代码进入阻塞状态,等待上一个线程的释放。
接下来我们来说一下synchronized的实现方式,有两种实现方式:
第一种,在方法签名处加synchronized关键字。
首先我们先来看一下不加synchronized关键字的运行效果创建一个synchronizedthread测试线程类。
创建一个测试类,创建并启动线程。
运行结果为。
我们可以看到,两个线程是交替插入数据的,现在我们在synchronizedthread类中的insert方法签名处加synchronized关键字,再运行一下。
运行结果。
这时,我们可以看到,线程1运行插入完毕后,线程而才开始插入。synchronized关键字起到了锁定对象的效果。
这里说一下关于一些使用synchronized方法需要注意的问题,当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。
如果一个线程a需要访问对象object1的synchronized方法1,另外一个线程b需要访问对象object2的synchronized方法1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
第二种使用synchronized(对象或类){代码}进行同步,这是一个同步代码块,不会锁住整个方法,只会锁住包含的代码。使用原则是尽量的缩小锁住的范围,使锁的时间尽量短,能用同步代码块,就不用同步关键字。
我们来改造一下第一个代码案例中的synchronizedthread类。
其中synchronized(对象或类)里的对象或类可以new 一个对象,或者使用this表示当前对象的锁。
这里的同步代码块更加灵活,不会锁住整个方法,提高执行效率。另外对于静态同步方法,是与上面的同步方式有所区别的,静态同步方法使用的是当前对象的class对象。
总结一下就是java中的每一个对象都可以作为锁。
1、对于同步方法,锁是当前实例对象。
2、对于静态同步方法,锁是当前对象的class对象。
3、对于同步方法块,锁是synchonized括号里配置的对象。