多线程——锁机制(线程同步)

8 篇文章 0 订阅
5 篇文章 0 订阅

接上一章,本章来学习多线程的重中之重,锁!

概述

多线程的同步问题,几乎是多线程开发中不可避免要面对的问题。
锁机制,就是为解决多线程同步问题而生。
简单来说,锁机制就是类似于现实生活中的锁。对某段代码上锁,留出一把钥匙。拿到这把钥匙的线程,就可以开锁,执行这段代码。拿不到钥匙的线程,就只能等待钥匙用完后被分享出来,再去争取得到这把钥匙来开锁。
如此往复,就能保证多个线程,轮流着执行这段代码(每个线程对这段代码的整个执行过程是不会被打断的,所以是同步的)。就不会出现多个线程同时在执行这段代码,互相打断,造成混乱的情况。一切因为上了锁而变得有序和同步。

综上,线程同步的重点就在于
的重点就在于:
1、获取锁(得到执行的许可)
2、等待锁(维护秩序,保证执行时没有干扰)
3、释放锁(给下一个线程来执行)
多线程的锁机制,就是从这三点出发,通过各种方式,来达到锁的效果。

1、synchronized关键字

synchronized关键字是线程安全的一种方式,通过synchronized可以为某块具体代码上锁,要调用到这块代码,就必须先获取到锁,获取不到,就必须等待。

  • 使用synchronized关键字修饰的方法代码块,就是对其加了锁。(一般都是对象锁;如果是修饰在了static关键字上,加的就是类锁。)
    在锁未被释放的时候,任何用到同一个锁的地方都无法被其他线程调用。
    即,两个synchronized方法使用了同一个对象锁,那么即使只有一个方法被线程[A]调用,其他线程[B]、[C]等也无法调用另一个方法,因为其他线程获取不到锁,锁正在被[A]占用着。

  • 同步方法同步代码块的区别:
    同步方法:synchronized关键字加在方法上,默认当前对象作为锁。
    同步代码块:synchronized关键字套在具体代码块上,需显式指定任意对象作为锁。

  • 类锁对象锁的区别:
    类锁:
    1、synchronized关键字加在static关键字上的,调用的就是类锁。
    2、在同步代码块自定义锁的时候,使用类名.class也可以调用类锁。
    对象锁:
    除了类锁之外的,其他都是对象锁。

各种synchronized的实现方式如下:

	public class service{
		//1、synchronized同步代码块(对象锁)
		public void synMethod() {
            synchronized(this) {	//this:调用的是当前类对象的对象锁
                //do something ...
            }  
        }
        
		//2、synchronized同步方法(对象锁)
		public synchronized void synMethod2() { //不需要显式声明,默认调用当前类对象的对象锁
            //do something ... 
        }
        
		//3、synchronized同步静态方法(类锁)
		public static synchronized void synMethod3() { //不需要显式声明,默认调用当前类的类锁
            //do something ... 
        }
	}
  • 可重入锁(是锁在使用过程中的一种逻辑,并不是锁的特性。)
    在线程获取到对象锁之后,此时这个锁还未释放,且再次需要获取这个锁(如调用了其他同步方法),那么这个锁是可重复获取的。
    即在锁未释放的时候,任何用到这个锁的地方,线程都可以进入,因为锁已经获取到了。
    注:可重入锁也支持在父子类继承中(子类同步方法获取到锁之后,同样可以调用父类同步方法)。

  • 出现异常,锁自动释放

  • 同步方法不具备继承性:就算父类方法是synchronized,子类继承后,没有显式加上synchronized关键字,子类这个方法就不是同步方法。

2、synchronized

前面说到,静态同步方法,加的是类锁,该类下的任何类实例都是调用的同一个类锁。
那如果同时使用了类锁和该类对象的对象锁,这两个锁是同一个吗?答案是否定的。

每个锁都是唯一独立的,功能简单,可以仅仅看作是一把钥匙,用于开锁,除此之外无其他任何特性。
只需要保持这一个原则,其他的如:内部类、静态内部类等这些情况的锁,也能够明白了,这些锁都是唯一的。

所以就算同步方法获取了锁,同步代码块也需要等待这个锁。并不会因为不是同样的同步方式,就影响了锁的获取。

3、死锁

死锁就是永远获取不到锁,永远在等待。
造成死锁的原因有很多,举个例子:

  1. 线程A获取了A锁,正在等待B锁执行接下来的程序;
  2. 此时线程B已经获取了B锁,又在等待A锁来执行接下来的程序。
  3. 此时线程A要获取到B锁,才能执行完,执行完才能释放A锁
  4. 但是线程B已经占据了B锁,又需要线程A释放的A锁,来让线程B执行完,才能释放他的B锁线程A使用。
  5. 线程A、B互相等待,且永远都等待不到结果,这就造成了死锁。

代码参考

public class TestThreadLock {	
	public void sync1() {
		synchronized(this) {
			try {
				System.out.println("test1");
				Thread.sleep(3000);// 休眠3秒,以让步给线程b的sync2方法,让其获取到类锁
				synchronized(TestThreadLock.class) {
					System.out.println("test11");
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	public void sync2() {
		synchronized(TestThreadLock.class) {
			try {
				System.out.println("test2");
				Thread.sleep(3000); // 休眠3秒,以让步给线程a的sync1方法,让其获取到对象锁
				synchronized(this) {
					System.out.println("test21");
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}	
	
	public static void main(String[] args) {
		TestThreadLock t = new TestThreadLock();
		
		Thread a = new Thread(new Runnable() {
			public void run() {
				t.sync1();
			}
		});
		Thread b = new Thread(new Runnable() {
			public void run() {
				t.sync2();
			}
		});
		
		a.start();
		b.start();
	}
}
执行结果:
test1
test2

执行解析:
1、线程a调用的sync1方法上,有两个同步代码块,分别调用了当前对象锁和类锁(线程b同样);
2、线程a先执行,调用sync1,获取到当前对象t的对象锁,打印日志,休眠3秒;
3、在线程a休眠的时候,线程b开始执行,调用sync2,获取到类锁,打印日志,休眠3秒;
4、线程b休眠,又轮到了线程a继续执行,此时线程a需要获取到类锁,才能接着执行,但是类锁在线程b上,还未释放,线程a等待;
5、cpu切换,轮到线程b执行,此时线程b需要获取到当前对象t的对象锁,才能接着执行,但是对象锁在线程a上,还未释放,线程b等待;
6、cpu继续切换,又轮到线程a,又是同样的情况,继续等待类锁;
7、cpu继续切换,又轮到线程b,还是同样的情况,继续等待对象锁;
8、如此循环往复,线程a、b在互相等待对方的锁,但双方都释放不了自己的锁,这就造成了死锁;
9、所以控制台只打印了两条日志,就没有了后续,因为程序已经陷入死锁。

所以在使用锁的时候,也要尤其小心,避免死锁的情况。必须确保锁最终能够被释放。
但很多时候,我们的代码逻辑并没有像上面的例子一样清晰简单,在复杂的逻辑下,造成死锁会变得很难查证问题。
这时候可以借用工具辅助检测(Jconsole、Jstack)。

4、Lock

除了synchronized关键字,还有Lock接口可以实现锁。
同为锁,Locksynchronized的区别有:

  1. 从定义上,synchronized是java关键字,是jvm实现的。而Lock是由jdk实现的一个类,并不是内置的。
  2. 从使用上,synchronized是自动加锁、释放锁,遇到异常程序会自动释放。而Lock的加锁、解锁、获取等,每一步都需要自己手动操作,遇到异常,也需要自己处理锁的释放。

相比之下,Lock使用起来需要更多操作,但也提供了更多的灵活性。

  • Lock锁的一般用法:

    • lock():获取锁,如果锁已被其他线程获取,则进行等待。
    • unLock():释放锁。
    • tryLock():有返回值的获取锁。如果获取成功,则返回true,如果获取失败,则返回false。无论获不获取到锁,都会立即返回,就算拿不到锁也不会一直等待。
    • tryLock(long time, TimeUnit unit):类似tryLock(),只是多了等待时间。在设置的time等待时间内,获取到锁了,就返回true;一直获取不到锁,就返回false
    • newCondition():等待/唤醒的配合,下一节介绍。
  • 发生异常时,程序不会自动释放锁。所以一般使用 lock()时必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
    使用例子

    //lock() 的使用 ========================
    Lock lock = new ReentrantLock();
    lock.lock();//获取锁
    try{
    	//do something...
    }catch (Exception e){
    	e.printStackTrace();
    }finally{
    	lock.unlock();//释放锁
    }
    
    //tryLock() 的使用 ========================
    Lock lock = new ReentrantLock();
    if(lock.tryLock(1, TimeUnit.SECONDS)){//设置获取锁的等待时长1秒
    	try{
    		//do something...
    	}catch (Exception e){
    		e.printStackTrace();
    	}finally{
    		lock.unlock();//释放锁
    	} 
    }else{
    	//do something ... 
    }
    

ReentrantLockLock的具体实现。
  1. ReentrantLock:实现了常规的Lock方法,用以进行锁的调用。
    new ReentrantLock():新建锁对象,默认为 非公平锁;
    new ReentrantLock(boolean isFair):新建锁对象,true=公平锁,false=非公平锁。
    公平锁:按照先来先得(FIFO)的顺序,分配锁。
    非公平锁:随机分配锁。
    举个例子

    //公平锁
    public class TestThreadReentrantLock {
    	private ReentrantLock lock;
    	//通过初始化,创建锁
    	public TestThreadReentrantLock(boolean isFair) {
    		lock = new ReentrantLock(isFair);
    	}
    	//具体演示加锁释放锁的方法
    	public void server() {
    		lock.lock();
    		try {
    			System.out.println(Thread.currentThread().getName() + "获得了锁");
    		}finally {
    			lock.unlock();
    		}
    	}
    	//创建5个线程,演示公平/非公平锁
    	public static void main(String[] args) {
    		TestThreadReentrantLock t = new TestThreadReentrantLock(true); //输入true,创建公平锁
    		
    		Runnable r = new Runnable() {
    			public void run() {
    				System.out.println(Thread.currentThread().getName() + "开始运行");
    				t.server();
    			}
    		};
    		
    		Thread[] threads = new Thread[5];
    		for(int i = 0; i < 5; i++) {
    			threads[i] = new Thread(r);
    		}
    		for(int i = 0; i < 5; i++) {
    			threads[i].start();
    		}
    	}
    }
    执行结果:
    Thread-0开始运行
    Thread-4开始运行
    Thread-3开始运行
    Thread-1开始运行
    Thread-2开始运行
    Thread-0获得了锁
    Thread-4获得了锁
    Thread-3获得了锁
    Thread-1获得了锁
    Thread-2获得了锁
    
    执行解析:
    1、从结果可以看出,前5行是线程开始运行的日志,后5行是线程轮流获取锁的日志;
    2、获取锁的顺序,就是线程运行的顺序;
    3、这就是公平锁,根据线程进入队列的顺序,来公平地配给锁。
    
    //非公平锁
    1、将main()方法里创建测试类的入参改为false:
    	TestThreadReentrantLock t = new TestThreadReentrantLock(false); //输入false,创建非公平锁
    2、重新运行程序
    3、执行结果:
    	Thread-0开始运行
    	Thread-3开始运行
    	Thread-0获得了锁
    	Thread-1开始运行
    	Thread-4开始运行
    	Thread-2开始运行
    	Thread-1获得了锁
    	Thread-3获得了锁
    	Thread-4获得了锁
    	Thread-2获得了锁
    4、从结果可以发现,运行顺序是:0-3-1-4-2,获取锁顺序是:0-1-3-4-2
    5、非公平锁,获取锁的顺序是随机的,与线程进入队列的顺序无关
    
  2. ReentrantReadWriteLock:读写锁

    1. ReentrantReadWriteLock.readLock():读操作相关的锁,也称为共享锁。
      同一时间可以有多个线程同时读取锁后的内容。
      所以加锁的意义在于,在读取的时候,就不能进行写入了,保证读取数据的实时正确性。因为读锁的存在,导致在写入前调用的写锁,会获取不到。直到读完,解锁,才可以写。
      	ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
      	lock.readLock().lock();
      	lock.readLock().unlock();
      
    2. ReentrantReadWriteLock.writeLock():写操作相关的锁,也称为排他锁。只要有写锁,就是互斥的。
      	lock.writeLock().lock();
      	lock.writeLock().unlock();
      

    读写锁的意义
    单纯从读写锁的使用上来看,好像没必要使用他们。因为读锁可以重入,那么不加锁就好。写锁互斥,那么加一般的锁也一样。
    那为什么还会设计出读写锁来呢?
    关键的地方在于:
    1、读与写,用的是同一个锁。
    2、读操作和写操作,存在并发调用的情况。
    正是因为这同一个锁,用在了读和写之上,才可以将读、写统一起来管理。当读与写同时发生时,因为用的是同一个锁,读写互斥,所以读和写必定只能先执行完一个,才能执行下一个。这样就避免了,在读出数据的时候,又重新写入了数据,导致读出的数据变成了脏数据。
    同时,读写锁提供的读读不互斥,又使得在进行读操作时,可以同步读取数据,不会阻塞。提高了效率。
    这样的读、写配合,为相关的读写操作,提供了更为便利和高效的锁控制。

    举个例子,加深印象

    /**
     *	1、设计一个读方法,加读锁;
     *	2、设计一个写方法,加写锁;
     *	3、创建多个线程,看读读、写写、读写 之间的效果。
     */
    public class TestThreadReadWriteLock {
    	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    	//读
    	public void read() {
    		try {
    			lock.readLock().lock();
    			System.out.println(new Date() + "--" + Thread.currentThread().getName() + " - read ");
    			Thread.sleep(1000); // 休眠一秒,以查看是重入还是等待 的效果
    		}catch(Exception e) {
    			e.printStackTrace();
    		}finally {
    			lock.readLock().unlock();
    		}
    	}
    	//写
    	public void write() {
    		try {
    			lock.writeLock().lock();
    			System.out.println(new Date() + "--" + Thread.currentThread().getName() + " - write ");
    			Thread.sleep(1000); // 休眠一秒,以查看是重入还是等待 的效果
    		}catch(Exception e) {
    			e.printStackTrace();
    		}finally {
    			lock.writeLock().unlock();
    		}
    	}
    	
    	public static void main(String[] args) {
    		TestThreadReadWriteLock t = new TestThreadReadWriteLock();
    		
    		int length = 10;
    		Thread[] readThreads = new Thread[length];//读线程
    		Thread[] writeThreads = new Thread[length];//写线程
    		
    		for(int i = 0; i < length; i++ ) {
    			Thread readThread = new Thread(new Runnable() {
    				public void run() {
    					t.read();
    				}
    			});
    			readThreads[i] = readThread;
    			
    			Thread writeThread = new Thread(new Runnable() {
    				public void run() {
    					t.write();
    				}
    			});
    			writeThreads[i] = writeThread;
    		}
    		
    		try {
    			//读读不互斥
    			System.out.println("--- 读读不互斥 ---");
    			readThreads[0].start();
    			readThreads[1].start();
    			readThreads[4].start();
    			readThreads[5].start();
    			
    			Thread.sleep(8000); //休眠8秒,等前面的线程都执行完毕,再执行下一组测试
    			
    			//写写互斥
    			System.out.println("--- 写写互斥 ---");
    			writeThreads[0].start();
    			writeThreads[1].start();
    			writeThreads[4].start();
    			
    			Thread.sleep(8000);
    			
    			//读写互斥
    			System.out.println("--- 读写互斥 ---");
    			readThreads[2].start();
    			writeThreads[2].start();
    			readThreads[6].start();
    			writeThreads[5].start();
    			readThreads[7].start();
    		}catch(Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    执行结果:
    --- 读读不互斥 ---
    Thu Jun 18 09:39:07 CST 2020--Thread-2 - read 
    Thu Jun 18 09:39:07 CST 2020--Thread-10 - read 
    Thu Jun 18 09:39:07 CST 2020--Thread-8 - read 
    Thu Jun 18 09:39:07 CST 2020--Thread-0 - read 
    --- 写写互斥 ---
    Thu Jun 18 09:39:15 CST 2020--Thread-1 - write 
    Thu Jun 18 09:39:16 CST 2020--Thread-3 - write 
    Thu Jun 18 09:39:17 CST 2020--Thread-9 - write 
    --- 读写互斥 ---
    Thu Jun 18 10:55:54 CST 2020--Thread-12 - read 
    Thu Jun 18 10:55:54 CST 2020--Thread-4 - read 
    Thu Jun 18 10:55:55 CST 2020--Thread-5 - write 
    Thu Jun 18 10:55:56 CST 2020--Thread-11 - write 
    Thu Jun 18 10:55:57 CST 2020--Thread-14 - read 
    
    执行解析:
    0、从上面打印的日志可以看出:
    1、“读读不互斥”日志里:每个读锁基本都是同一时间获得,并不需要等待读锁方法里休眠时间的结束。
    说明,读锁是可重入的。
    2、“写写互斥”日志里:每个写锁,进入时间都不同,都是等待上一个写锁进入1秒(写锁方法的休眠时间)后,再进入。
    说明,写锁是互斥的。
    3、“读写互斥”日志里:因为线程是按照读、写、读、写、读的顺序开启的。所以读写锁的日志理应是交错的。
    但实际上,日志先打印了两个读操作,隔了1秒后,才打印了写操作。
    说明,当读写操作并发时,读操作可以重入,但写操作,必须等锁释放了,才可以进入。
    尽管读、写操作调用的叫读锁和写锁,但本质上其实是同一个锁。只是为不同的操作,有不同的效果。如读操作,是可重入的效果。写操作,是互斥的效果。
    再继续看日志,可以发现,写操作1秒后,又开始下一个写操作,隔1秒后,再执行最后一个读操作。
    再一次说明,读、写操作之间是互斥的,必须得等到一个操作释放锁了,才能获取到锁进行下一个操作。
    
  3. locksynchronized的区别
    1. 用法:synchronized既可以很方便的加在方法上,也可以加载特定代码块上,而lock需要显示地指定起始位置和终止位置。
    2. 实现:synchronized是依赖于JVM实现的,而ReentrantLock是JDK实现的。
    3. 性能:synchronizedlock其实已经相差无几,其底层实现已经差不多了。
    4. 功能:ReentrantLock功能更强大,可操作性更高。

    • ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁。
    • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
    • ReentrantLock提供了一个Condition类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

:在遇到需要控制线程同步的时候,建议优先考虑synchronized,如果有特殊需求,再考虑ReentrantLock。因为ReentrantLock中如锁的释放等,都需要手动操作,容易引起问题。如果用的不好,不仅不能提高性能,还可能带来灾难。

5、Condition(对象监视器)

使用Condition可以对Lock锁进行等待/唤醒。类似于synchronizedwait/notify的关系,Condition是属于Lock的等待/唤醒机制。
比起synchronized更灵活的是,Condition可以为每个线程,各自注册指定的Condition(可以一对一,也可以一对多),从而实现对指定线程的等待和唤醒。
synchronized的等待唤醒,是随机的。可以看作是只有一个Condition,这个Condition上注册了所有的线程,notify/notifyAll的时候,也只有随机唤醒这之中的一个线程。
因而相比之下,ConditionLock的结合,为线程的等待/唤醒,提供了更高的可操作性。

创建

  • 依赖于Lock接口,通过lock.newCondition()新建。
  • 调用Condition的方法前,必须先lock()获得锁,否则会抛出IllegalMonitorStateException异常。

主要方法

  • Condition.await() : 类似于Object.wait(),释放锁,并加入等待队列
  • Condition.signal() : 类似于Object.notify(),唤醒。 根据等待队列先进先出(FIFO)原则,按顺序进行唤醒。
  • Condition.signalAll():类似Object.notifyAll(),对等待队列中的每个线程均执行一次signal()方法,将等待队列中的线程全部移动到阻塞队列中,并唤醒队列中的每个线程

原理
ConditionAbstractQueuedSynchronizer的内部类,AbstractQueuedSynchronizer类里有一个阻塞队列和n个等待队列
阻塞队列存放着等待锁(等待运行)的线程,头部线程是正在运行的线程,按照FIFO原则,顺序执行队列里的线程。
等待队列存放着调用了await()的线程,每个Condition都有属于自己的等待队列。

  • Condition调用await()方法时,就会将当前线程存入到Condition等待队列尾部,同时释放锁,等待唤醒。这时候,线程就与Condition建立起了联系。
  • Condition调用signal()方法时,会将等待队列的第一个线程(按照FIFO原则)加入到阻塞队列尾部,等待被唤醒,获取锁。
  • Condition调用signalAll()方法时,也是类似signal()的逻辑,会将等待队列内的线程按顺序全部释放,并按照顺序加入到阻塞队列

补充说明
0、通过源码可以发现:
1、ReentrantLocknewCondition()方法,实际上是调用了内部类Sync对象的newCondition()方法。
2、而Sync类继承了AbstractQueuedSynchronizer类。
3、AbstractQueuedSynchronizer内部有一个阻塞队列,所以Sync内部也继承了这个阻塞队列(就是Condition唤醒线程所去到的阻塞队列)。
4、所以由Sync创建的Condition与阻塞队列属于多对一的关系。即Sync可以创建多个Condition,但Sync只有一个阻塞队列。
5、而调用Sync对象的Lock对象,与阻塞队列属于一对一的关系。因为一个Lock对象,内部只有一个Sync对象。
6、所以不同的Lock对象拥有各自的阻塞队列。
综上所述:

  • 阻塞队列是跟lock关联,一个lock对应一个阻塞队列。
  • 等待队列跟condition关联,一个condition对应一个等待队列。
  • 线程是通过condition.await(),与conditionlock关联。
  • 同一个lock下的condition,所有的signal/signalAll操作,都会按照先后顺序对线程进行唤醒执行。
  • 不同lock下的condition,其signal/signalAll操作,存在于不同的阻塞队列,各阻塞队列之间存在互相争夺资源的情况,因而其唤醒后的线程执行,是无序的。

举个例子,加深印象

/**
 * 验证同个阻塞队列、不同阻塞队列,线程的唤醒顺序。
 * 验证condition可以对对应线程进行等待、唤醒操作。
 */
public class TestThreadCondition {
	private ReentrantLock lock = new ReentrantLock();
	private ReentrantLock lock3 = new ReentrantLock();
	public Condition c1 = lock.newCondition();//与c2在同一个阻塞队列
	public Condition c2 = lock.newCondition();//在c1在同一个阻塞队列
	public Condition c3 = lock3.newCondition();//在另外的阻塞队列
	//调用c1的等待
	public void await1() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "--await1 start ");
			c1.await();
			System.out.println(Thread.currentThread().getName() + "--await1 end ");
		}catch (InterruptedException e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
	}
	//调用c2的等待
	public void await2() {
		lock.lock();
		try {			System.out.println(Thread.currentThread().getName() + "--await2 start ");
			c2.await();
			System.out.println(Thread.currentThread().getName() + "--await2 end ");
		}catch (InterruptedException e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
	}
	//调用c3的等待
	public void await3() {
		lock3.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "--await3 start ");
			c3.await();
			System.out.println(Thread.currentThread().getName() + "--await3 end ");
		}catch (InterruptedException e) {
			e.printStackTrace();
		}finally {
			lock3.unlock();
		}
	}
	//唤醒所有c1的线程
	public void signalAll1() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "--signalAll1 ");
			c1.signalAll();
		}finally {
			lock.unlock();
		}
	}
	//唤醒所有c2的线程
	public void signalAll2() {
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "--signalAll2 ");
			c2.signalAll();
		}finally {
			lock.unlock();
		}
	}
	//唤醒所有c3的线程
	public void signalAll3() {
		lock3.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "--signalAll3 ");
			c3.signalAll();
		}finally {
			lock3.unlock();
		}
	}
	/**
	 * 1、针对c1、c2、c3先各自创建5个线程
	 * 2、各个线程内部调用await()方法
	 * 3、执行创建的线程
	 * 4、分别调用signalAll,唤醒3个condition的所有线程
	 */
	public static void main(String[] args) {
		TestThreadCondition t = new TestThreadCondition();
		
		int length = 5;
		Thread[] thread1s = new Thread[length];//绑定condition1
		Thread[] thread2s = new Thread[length];//绑定condition2
		Thread[] thread3s = new Thread[length];//绑定condition3
		
		for(int i = 0; i < length; i++) {
			Thread thread1 = new Thread(new Runnable() {
				public void run() {
					t.await1();
				}
			});
			thread1.setName("Thread1-"+i);
			thread1s[i] = thread1;
			
			Thread thread2 = new Thread(new Runnable() {
				public void run() {
					t.await2();
				}
			});
			thread2.setName("Thread2-"+i);
			thread2s[i] = thread2;
			
			Thread thread3 = new Thread(new Runnable() {
				public void run() {
					t.await3();
				}
			});
			thread3.setName("Thread3-"+i);
			thread3s[i] = thread3;
		}
		
		for(int i = 0; i < length; i++) {
			thread1s[i].start();
			thread2s[i].start();
			thread3s[i].start();
		}
		
		try {
			Thread.sleep(1000);
			//唤醒
			ReentrantLock l = new ReentrantLock();
			l.lock();
			try {
				t.signalAll1();
				t.signalAll2();
				t.signalAll3();
			}finally {
				l.unlock();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

执行结果:
Thread1-0--await1 start 
Thread3-0--await3 start 
Thread3-2--await3 start 
Thread2-2--await2 start 
Thread3-3--await3 start 
Thread1-2--await1 start 
Thread2-3--await2 start 
Thread3-1--await3 start 
Thread1-4--await1 start 
Thread2-1--await2 start 
Thread2-0--await2 start 
Thread1-1--await1 start 
Thread3-4--await3 start 
Thread1-3--await1 start 
Thread2-4--await2 start 
main--signalAll1 
main--signalAll2 
main--signalAll3 
Thread1-0--await1 end 
Thread3-0--await3 end 
Thread3-2--await3 end 
Thread3-3--await3 end 
Thread3-1--await3 end 
Thread1-2--await1 end 
Thread1-4--await1 end 
Thread1-1--await1 end 
Thread1-3--await1 end 
Thread2-2--await2 end 
Thread3-4--await3 end 
Thread2-3--await2 end 
Thread2-1--await2 end 
Thread2-0--await2 end 
Thread2-4--await2 end 

执行解析:
1、从执行结果可以看出,执行完3个signalAll操作之后,所有线程被唤醒;

2、被唤醒的线程中,先忽略Thread3线程,可以发现,Thread1线程先全部被唤醒后,才轮到Thread2线程被唤醒;

32证明了,Thread1和Thread2是在同一个阻塞队列中的。
由于Thread1先调用signalAll操作,所以Thread1线程先进入阻塞队列,Thread2线程后进入。
而阻塞队列按顺序执行,最终结果就如上所示,Thread1线程先全部执行完,才轮到Thread2;

4、回来看Thread3线程。Thread3线程夹杂在Thread1、2线程之间,看似无序地执行;

5、由4可以看出,Thread3线程的执行与Thread1、2没有任何联系。
因为Thread3是由lock3的condition唤醒,而Thread1、2是由lock的condition唤醒。
说明,不同lock的阻塞队列不同,所以不同lock下的唤醒是无序的,会互相抢占资源;

6、再来分别观察Thread1、23里面的所有线程。
可以发现,各个Threadx下面的线程,都是按照其等待(awaitx start)的顺序,被唤醒(awaitx end)的。
说明,condition调用await进入等待队列是有序的,调用signal唤醒线程也是有序的,且都是按照先进先出(FIFO)的顺序。

7、从各个Threadx线程调用的awaitx/signalAllx方法,可以发现,各个condition的await与其signal/signalAll是对应的。
说明,同一个condition调用的signal/signalAll,只能唤醒调用了自己的await的线程,影响不了其他condition的线程。
这也证明了,condition可以对指定线程进行等待唤醒操作。

6、volatile关键字

当 JVM 设置为-server模式的时候,线程在内部定义了私有变量,这个私有变量就会存在于公共内存该线程的私有内存中。
-server模式下的 JVM 为了提高线程执行效率,会让线程一直读取其私有内存数据。这就导致了公共内存该线程的私有内存中变量的值不同步:
1、在该线程中读取,则读取的是该线程的私有内存中的变量;
2、在该线程外读取,则读取的是公共内存中的变量。
如图
线程私有变量的位置
为了保证这种情况下的数据同步,可以使用volatile关键字。
使用volatile,直接声明在属性上即可。
如:

public volatile int i = 1;

这样,就可以使得线程内、外获取到的私有变量是同步的了。

之所以会将volatile放在这章介绍,是因为volatile关键字也可以当成是一种锁的方式。因为它可以保证在所有地方调用元素的时候,都能得到最新的值,保证了值的同步。是一种类似的读锁。
但是volatile又不是传统意义上的锁,因为它不会对元素加锁,不会对线程阻塞。相比synchronizedvolatile是更加轻量级的同步机制。
synchronized具有volatile功能,在synchronized内操作的变量,可以将线程内存和公共内存同步。

加了volatile关键字后,私有变量的读取就变成了强制从公共内存中读取。
如图
加入volatile关键字

但是,就如上所说,volatile只是保证元素在任何地方读取的时候都是最新的。所以它并不能保证操作元素的时候是同步的。
即便是如i++这样的操作。
因为i++也就是i=i+1,可以具体拆分为以下三个步骤:
1、获取i的值。 这里加了volatile关键字,所以可以保证取到的值就是最新的;
2、计算i+1 这个地方是没有加锁的,所以如果此刻其他线程也在计算i的值,这里的i+1中的i可能就不是上一步获取到的i了;
3、将计算后的i写入内存。 有可能第2步的时候计算出来的i值就已经不是最新的了,也有可能计算出来i值之后,又有其他线程在修改i值,这时写入内存的i就会造成其他线程修改后的i变成了脏数据。或者可能在它写入内存后,其他线程又马上也把他们修改后的i也写入了内存,导致i++这个计算的结果没有保存到。

综上所述,volatile只能保证读取到的是最新的数据,并不能保证对数据的操作是一个整体不被其他线程异步影响。也就是说,volatile是不具备原子性的。

7、原子操作

就如上面说到的,volatile不具备原子性。那么什么是原子性呢?
原子性就是使操作作为一个不可分割的整体,就像一个原子,不能再被异步拆分。类似加了synchronized锁一样的效果。
那么要使类似i++这样的操作保持原子性,要怎么做呢?答案就是atomic原子类。

它类似加了synchronized关键字的计算,每个原子操作都是不可分割,线程安全、同步的。但它又不像synchronized,因为它并没有加锁。
常用的原子类有如下几种:

  • AtomicInteger:操作int类型
  • AtomicLong:操作long类型
  • AtomicBoolean:操作boolean类型
  • 等等

常用方法参考如下:

  • getAndIncrement():先获取到了值,再默默加1,如i++
  • incrementAndGet():先加1,再获取加完的结果值,如++i
  • addAndGet(int delta):加具体值,再获取结果,如i=i+3
  • 等等

例子

	AtomicInteger i = new AtomicInteger(100);
    int val = i.getAndIncrement();
    System.out.println(val); // 100
    System.out.println(i.intValue()); // 101
    val = i.addAndGet(9);
    System.out.println(val); // 110

8、查询锁信息的相关方法

  1. lock.getHoldCount():查询当前线程调用这个锁的lock()方法的次数;
  2. lock.getQueueLength():返回正在等待这个锁的线程的估计数;
  3. lock.getWaitQueueLength(Condition condition):返回等待与这个锁相关的condition(调用了await())的线程估计数;
  4. lock.hasQueuedThread(Thread thread):查询指定的线程thread是否正在等待获取这个锁;
  5. lock.hasQueuedThreads():查询是否有线程正在等待获取这个锁;
  6. lock.hasWaiters(Condition condition):查询是否有线程调用了这个conditionawait()方法,正在等待中;
  7. lock.isFair():判断这个锁是不是公平锁;
  8. lock.isHeldByCurrentThread():查看当前线程是否正持有这个锁;
  9. lock.isLocked():查看这个锁是否有被线程占用着;
  10. lock.lockInterruptibly() :如果当前线程未被中断,则获取锁,如果已经中断了,则抛出异常;
  11. condition.awaitUninterruptibly():与await()类似,但是调用此方法之后,再调用interrupt()中断不会报错。调用await()之后调用interrupt()中断会报错。
  12. condition.awaitUntil(Date deadline):与await()类似,等待deadline时间后,自动唤醒。且在这期间,也可以被signal/signalAll唤醒。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值