synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
三、独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
四、互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock
五、乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
六、分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
七、偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
八、自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
线程自旋和适应性自旋
我们知道,java线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。
并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。
这样就避免了线程切换的开销,极大的提升了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。
他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
二.synchronized使用示例
1.多窗口售票
假设一个火车票售票系统,有若干个窗口同时售票,很显然在这里票是作为多个窗口的共享资源存在的,由于座位号是确定的,因此票上面的号码也是确定的,我们用多个线程来模拟多个窗口同时售票,首先在不使用synchronized关键字的情况下测试一下售票情况。
先将票本身作为一个共享资源放在单独的线程中,这种作为共享资源存在的线程很显然应该是实现Runnable接口,我们将票的总数num作为一个入参传入,每次生成一个票之后将num做减法运算,直至num为0即停止,说明票已经售完了,然后开启多个线程将票资源传入。
public class Ticket implements Runnable{
private int num;//票数量
private boolean flag=true;//若为false则售票停止
public Ticket(int num){
this.num=num;
}
@Override
public void run() {
while(flag){
ticket();
}
}
private void ticket(){
if(num<=0){
flag=false;
return;
}
try {
Thread.sleep(20);//模拟延时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出当前窗口号以及出票序列号
System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
}
}
public class MainTest {
public static void main(String[] args) {
Ticketticket = new Ticket(5);
Threadwindow01 = new Thread(ticket, "窗口01");
Threadwindow02 = new Thread(ticket, "窗口02");
Threadwindow03 = new Thread(ticket, "窗口03");
window01.start();
window02.start();
window03.start();
}
}
程序的输出结果如下:
窗口02售出票序列号:5
窗口03售出票序列号:4
窗口01售出票序列号:5
窗口02售出票序列号:3
窗口01售出票序列号:2
窗口03售出票序列号:2
窗口02售出票序列号:1
窗口03售出票序列号:0
窗口01售出票序列号:-1
从上面程序运行结果可以看出不但票的序号有重号而且出票数量也不对,这种售票系统比12306可要烂多了,人家在繁忙的时候只是刷不到票而已,而这里的售票系统倒好了,出票比预计的多了而且会出现多个人争抢做同一个座位的风险。如果是单个售票窗口是不会出现这种问题,多窗口同时售票就会出现争抢共享资源因此紊乱的现象,解决该现象也很简单,就是在ticket()方法前面加上synchronized关键字或者将ticket()方法的方法体完全用synchronized块包括起来。
//方式一
private synchronized void ticket(){
if(num<=0){
flag=false;
return;
}
try {
Thread.sleep(20);//模拟延时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
}
//方式二
private void ticket(){
synchronized (this) {
if (num <= 0) {
flag = false;
return;
}
try {
Thread.sleep(20);//模拟延时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出票序列号:" + num--);
}
}
再看一下加入synchronized关键字的程序运行结果:
窗口01售出票序列号:5
窗口03售出票序列号:4
窗口03售出票序列号:3
窗口02售出票序列号:2
窗口02售出票序列号:1
从这里可以看出在实例方法上面加上synchronized关键字的实现效果跟对整个方法体加上synchronized效果是一样的。 另外一点需要注意加锁的时机也非常重要 ,本示例中ticket()方法中有两处操作容易出现紊乱,一个是在if语句模块,一处是在num–,这两处操作本身都不是原子类型的操作,但是在使用运行的时候需要这两处当成一个整体操作,所以synchronized将整个方法体都包裹在了一起。如若不然,假设num当前值是1,但是窗口01执行到了num–,整个操作还没执行完成,只进行了赋值运算还没进行自减运算,但是窗口02已经进入到了if语句模块,此时num还是等于1,等到窗口02执行到了输出语句的时候,窗口01的num–也已经将自减运算执行完成,这时候窗口02就会输出序列号0的票。再者如果将synchronized关键字加在了run方法上面,这时候的操作不会出现紊乱或者错误,但是这种加锁方式无异于单窗口操作,当窗口01拿到锁进入run()方法之后,必须等到flag为false才会将语句执行完成跳出循环,这时候的num就已经为0了,也就是说票已经被售卖完了,这种方式摒弃了多线程操作,违背了最初的设计原则-多窗口售票。
2.懒汉式单例模式
创建单例模式有很多中实现方式,本文只讨论懒汉式创建。在Android开发过程中单例模式可以说是最常使用的一种设计模式,因为它操作简单还可以有效减少内存溢出。下面是懒汉式创建单例模式一个示例:
(懒汉式与饿汉式的区别:Singleton 单例模式(懒汉方式和饿汉方式))
public class Singleton {
private static Singletoninstance;
private Singleton() {
}
public static SingletongetInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果对于多窗口售票逻辑已经完全明白了的话就可以看出这里的实现方式是有问题的,我们可以简单的创建几个线程来获取单例输出对象的hascode值。
com.sunny.singleton.Singleton@15c330aa
com.sunny.singleton.Singleton@15c330aa
com.sunny.singleton.Singleton@41aff40f
在多线程模式下发现会出现不同的对象,这种单例模式很显然不是我们想要的,那么根据上面多窗口售票的逻辑我们在getInstance()方法上面加上一个synchronized关键字,给该方法加上锁,加上锁之后可以避免多线程模式下生成多个不同对象,但是同样会带来一个效率问题,因为不管哪个线性进入getInstance()方法都会先获得锁,然后再次释放锁,这是一个方面,另一个方面就是只有在第一次调用getInstance()方法的时候,也就是在if语句块内才会出现多线程并发问题,而我们却索性将整个方法都上锁了。讨论到这里就引出了另外一个问题,究竟是synchronized方法好还是synchronized代码块好呢? 有一个原则就是锁的范围越小越好 ,加锁的目的就是将锁进去的代码作为原子性操作,因为非原子操作都不是线程安全的,因此synchronized代码块应该是在开发过程中优先考虑使用的加锁方式。
public static SingletongetInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
这里也会遇到类似上面的问题,多线程并发下回生成多个实例,如线程A和线程B都进入if语句块,假设线程A先获得锁,线程B则等待,当new一个实例后,线程A释放锁,线程B获得锁后会再次执行new语句,同样不能保证单例要求,那么下面代码再来一个null判断,进行双重检查上锁呢?
public static SingletongetInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
该模式就是双重检查上锁实现的单例模式,这里在代码层面我们已经 基本 保证了线程安全了,但是还是有问题的, 双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现bug,而是归咎于java平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。 更为详细的介绍可以参考 Java单例模式中双重检查锁的问题 。所以单例模式创建比较建议使用恶汉式创建或者静态内部类方式创建。
3.synchronized不具有继承性
我们可以通过一个简单的demo验证这个问题,在一个方法中顺序的输出一系列数字,并且输出该数字所在的线程名称,在父类中加上synchronized关键字,子类重写父类方法测试一下加上synchronized关键字和不加关键字的区别即可。
public class Parent {
public synchronized void test() {
for (int i = 0; i < 5; i++) {
System.out.println("Parent " + Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
子类继承父类Parent,重写test()方法.
public class Child extends Parent {
@Override
public void test() {
for (int i = 0; i < 5; i++) {
System.out.println("Child " + Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试代码如下:
final Child c = new Child();
new Thread() {
public void run() {
c.test();
};
}.start();
new Thread() {
public void run() {
c.test();
};
}.start();
输出结果如下:
Parent Thread-0:0 Child Thread-0:0
Parent Thread-0:1 Child Thread-1:0
Parent Thread-0:2 Child Thread-0:1
Parent Thread-0:3 Child Thread-1:1
Parent Thread-0:4 Child Thread-0:2
Parent Thread-1:0 Child Thread-1:2
Parent Thread-1:1 Child Thread-0:3
Parent Thread-1:2 Child Thread-1:3
Parent Thread-1:3 Child Thread-0:4
Parent Thread-1:4 Child Thread-1:4
通过输出信息可以知道,父类Parent中会将单个线程中序列号输出完成才会执行另一个线程中代码,但是子类Child中确是两个线程交替输出数字,所以synchronized不具有继承性。
4.死锁示例
死锁是多线程开发中比较常见的一个问题。若有多个线程访问多个资源时,相互之间存在竞争,就容易出现死锁。下面就是一个死锁的示例,当一个线程等待另一个线程持有的锁时,而另一个线程也在等待该线程锁持有的锁,这时候两个线程都会处于阻塞状态,程序便出现死锁。
package com.lock;
class Thread01 extends Thread{
private Object resource01;
private Object resource02;
public Thread01(Object resource01, Object resource02) {
this.resource01 = resource01;
this.resource02 = resource02;
}
@Override
public void run() {
synchronized(resource01){
System.out.println("Thread01 locked resource01");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource02) {
System.out.println("Thread01 locked resource02");
}
}
}
}
class Thread02 extends Thread{
private Object resource01;
private Object resource02;
public Thread02(Object resource01, Object resource02) {
this.resource01 = resource01;
this.resource02 = resource02;
}
@Override
public void run() {
synchronized(resource02){
System.out.println("Thread02 locked resource02");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource01) {
System.out.println("Thread02 locked resource01");
}
}
}
}
public class deadlock {
public static void main(String[] args) {
final Object resource01="resource01";
final Object resource02="resource02";
Thread01 thread01=new Thread01(resource01, resource02);
Thread02 thread02=new Thread02(resource01, resource02);
thread01.start();
thread02.start();
}
}
结果为:
Thread02 locked resource02
Thread01 locked resource01
执行上面的程序就会一直等待下去,出现死锁。当线程Thread01获得resource01的锁后,等待500ms,然后尝试获取resource02的锁,但是此时resouce02锁已经被Thread02持有,同样Thread02也等待了500ms尝试获取resouce01锁,但是该所已经被Thread01持有,这样两个线程都在等待对方所有的资源,造成了死锁。
三.其它
关键字synchronized具有锁重入功能,当一个线程已经持有一个对象锁后,再次请求该对象锁时是可以得到该对象的锁的,这种方式是必须的,否则在一个synchronized方法内部就没有办法调用该对象的另外一个synchronized方法了。锁重入是通过为每个所关联一个计数器和一个占有它的线程,当计数器为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM会记录锁的占有者,并将计数器设置为1。如果同一个线程再次请求该锁,计数器会递增,每次占有的线程退出同步代码块时计数器会递减,直至减为0时锁才会被释放。
在声明一个对象作为锁的时候要注意字符串类型锁对象,因为字符串有一个常量池,如果不同的线程持有的锁是具有相同字符的字符串锁时,两个锁实际上同一个锁。
ReentrantLock特性
轮询锁的和定时锁
可轮询和可定时的锁请求是通过tryLock()方法实现的,和无条件获取锁不一样. ReentrantLock可以有灵活的容错机制.死锁的很多情况是由于顺序锁引起的, 不同线程在试图获得锁的时候阻塞,并且不释放自己已经持有的锁, 最后造成死锁. tryLock()方法在试图获得锁的时候,如果该锁已经被其它线程持有,则按照设置方式立刻返回,而不是一直阻塞等下去,同时在返回后释放自己持有的锁.可以根据返回的结果进行重试或者取消,进而避免死锁的发生.
公平性
ReentrantLock构造函数中提供公平性锁和非公平锁(默认)两种选择。所谓公平锁,线程将按照他们发出请求的顺序来获取锁,不允许插队;但在非公平锁上,则允许插队:当一个线程发生获取锁的请求的时刻,如果这个锁是可用的,那这个线程将跳过所在队列里等待线程并获得锁。我们一般希望所有锁是非公平的。因为当执行加锁操作时,公平性将讲由于线程挂起和恢复线程时开销而极大的降低性能。考虑这么一种情况:A线程持有锁,B线程请求这个锁,因此B线程被挂起;A线程释放这个锁时,B线程将被唤醒,因此再次尝试获取锁;与此同时,C线程也请求获取这个锁,那么C线程很可能在B线程被完全唤醒之前获得、使用以及释放这个锁。这是种双赢的局面,B获取锁的时刻(B被唤醒后才能获取锁)并没有推迟,C更早地获取了锁,并且吞吐量也获得了提高。在大多数情况下,非公平锁的性能要高于公平锁的性能。
可中断获锁获取操作
lockInterruptibly方法能够在获取锁的同时保持对中断的响应,因此无需创建其它类型的不可中断阻塞操作。
读写锁ReadWriteLock
ReentrantLock是一种标准的互斥锁,每次最多只有一个线程能持有锁。读写锁不一样,暴露了两个Lock对象,其中一个用于读操作,而另外一个用于写操作。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
可选择实现:
1.释放优先
2.读线程插队
3.重入性
4.降级
5.升级
ReentrantReadWriteLock实现了ReadWriteLock接口,构造器提供了公平锁和非公平锁两种创建方式。读写锁适用于读多写少的情况,可以实现更好的并发性。
参考:Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁