本篇主要总结同步器的相关例子:包括synchronized、volatile、原子变量类(AtomicXxx)、CountDownLatch、ReentrantLock和ThreadLocal。还涉及到wait和notify/notifyAll。
回忆关于线程的几个基本知识点:
◆ 线程的概念(程序中不同的执行路径可以放到不同的CPU中同步运行);
◆ 如何启动一个线程(继承Thread类 / 实现Runnable接口 / 实现Callable接口,,,调用start()方法);
◆ 基本的线程同步方式 (synchronized——锁定的是一个对象、...)
对于synchronized的理解:
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
目录
♣ 例1.5:同步方法与非同步方法是否可以同时调用?—— 可以
♣ 例1.6:对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题。
♣ 例1.7:一个同步方法可以调用另外一个同步方法吗——可以
♣ 例1.8:在继承中,子类重写的同步方法可以调用父类的同步方法
♣ 例2.3 synchronized既保证可见性又保证了原子性
♣例2.4 实现一个容器,提供两个方法add、size,线程1添加10个元素到容器,线程2监控元素的的个数,当个数为5个时,线程2提示并结束。
♣♣ 2.4.2 使用wait()和notify()做到,wait释放锁,而notify不会释放锁
♣♣ 2.4.3 使用CountDownLatch(门闩)的await和countdown方法替代wait、notify方法来进行通知。
♣ 例3.1 原子性操作int类型——AtomicInteger
♣ 例4.1 ReentrantLock可以用来代替synchronized
♣ 例4.2 ReentrantLock可以进行尝试锁定tryLock()
♣ 例4.3 ReentrantLock可调用lockInterruptibly方法,对线程的interrupt方法作出响应,在一个线程等待的过程中,可以被打断。
♣ 例4.5 写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。
♣♣ 4.5.1 使用wait和notify/notifyAll()来实现
1.对某个对象加锁
♣ 例1.1:new一个对象作为锁
/**
* sychronized关键字
* 对某个对象加锁
*/
public class SyncronizeObject {
private int count = 10;
private Object o = new Object();
public void m() {
synchronized(o) { //任何线程要执行下面的代码,必须要先拿到o的锁
count -- ;
System.out.println(Thread.currentThread().getName()+"count = " + count);
}
}
例1 中,这把锁是自己new的,每次new出一个毫无其他功能的对象就当锁的对象比较麻烦。所以可以用synchronized(this)。
♣ 例1.2:锁定自身对象
public class SynchronizeThis {
private int count = 10;
public void m1() {
synchronized (this) { //任何线程要执行下面的代码,必须先拿到this的锁
count -- ;
System.out.println(Thread.currentThread().getName() + "count = " + count);
}
}
//m1()等同于下面的m2()
public synchronized void m2() { //等同于在方法的代码执行时要synchronized(this)
count -- ;
System.out.println(Thread.currentThread().getName() + "count = " +count);
}
}
♣ 例1.3:synchronized用在静态方法上
package cn.xx55xx.concurrence;
/*类中静态方法和静态属性属性是不需要new出对象来访问的,没有new出来,就没有this引用的存在,
所以当锁定一个静态方法时,相当于锁定的是当前类的class对象*/
public class SynchronizedStaticMethod {
private static int count = 10;
//等同于synchronized(cn.xx55xx.concurrence.SynchronizedStaticMethod.class)
public synchronized static void m1() {
count --;
System.out.println(Thread.currentThread().getName() + "count = " + count);
}
public static void m2() {
synchronized (SynchronizedStaticMethod.class) { //*.class是Class中的一个对象,这里是不能用synchronized(this)的
count -- ;
}
}
}
♣ 例1.4:锁住线程的run方法
public class SynchronizedThread implements Runnable{
private int count = 10;
@Override
public synchronized void run() {
count -- ;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
SynchronizedThread t = new SynchronizedThread();
for (int i=0; i<5; i++) {
new Thread(t,"THREAD-" + i).start();
}
}
}
run方法不加synchronized时,可能会出现以下这些情况(还有别的可能):
因为不加synchronized,count--和打印语句中间,有可能有别的线程来执行count--,导致前后数据不一致。加了synchronized这两条语句相当于是一个原子操作,一个run方法执行完毕释放了锁,下一个线程才能拿到锁执行run方法!
♣ 例1.5:同步方法与非同步方法是否可以同时调用?—— 可以
/*同步方法与非同步方法是可以同时调用的。只有synchronized修饰的方法在运行过程中才需要申请锁,普通方法是不需要申请的*/
public class AsynAndSynMethod {
public synchronized void m1() { //同步方法
System.out.println(Thread.currentThread().getName() + " m1.start... ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" m1 end");
}
public void m2() { //非同步方法
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" m2 ");
}
public static void main(String[] args) {
AsynAndSynMethod t = new AsynAndSynMethod();
new Thread(()->t.m1(),"t1").start(); //new Thread(t::m1,"t1").start();
new Thread(()->t.m2(),"t2").start(); //new Thread(t::m2,"t2").start();
}
}
执行结果:
♣ 例1.6:对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题。
/*对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题,读到在写的过程中还没有完成的数据,可以对读方法加锁*/
public class Account {
String name;
double balance; //账户余额为成员变量 默认为0.0
public synchronized void set(String name, double balance) { //写
this.name = name;
try {
Thread.sleep(2000); //2s
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public double getBalance(String name) { //读
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("张三",100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("张三")); //0.0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("张三")); //100.0
}
}
♣ 例1.7:一个同步方法可以调用另外一个同步方法吗——可以
/*一个同步方法可以调用另外一个同步方法。一个线程已经拥有某个对象的锁,再次申请的时候任然会得到该对象的锁,即synchronized获得的锁是可重入的。*/
public class SynMethodCallAnother {
synchronized void m1() {
System.out.println("m1 start...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
}
♣ 例1.8:在继承中,子类重写的同步方法可以调用父类的同步方法
/*一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到锁的对象,即可重入的。
在继承中,子类同步方法可以调用父类的同步方法*/
public class CallSuperclassSynMethod {
synchronized void m() {
System.out.println("m start...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end");
}
public static void main(String[] args) {
new Child().m(); //锁定的都是同一个对象(子类对象)
}
}
class Child extends CallSuperclassSynMethod {
@Override
synchronized void m() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
♣ 例1.9:出现异常,默认情况下锁会被释放,
/** 程序执行过程中,如果出现异常,默认情况锁会被释放,所以在并发处理的过程中,有异常要多加小心,不然会发生不一致的情况;
* 比如,在一个web application处理请求时,多个servlet线程共同访问同一个资源,这时如果异常处理不合适;
* 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生的数据,因此要非常小心地处理同步业务逻辑中的异常。*/
public class SynchronizedException {
int count = 0;
synchronized void m(){
System.out.println(Thread.currentThread().getName() + " start");
while(true) {
count ++ ;
System.out.println(Thread.currentThread().getName() + " count = " +count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
int i = 1/0; //此处抛出异常,锁将会被释放。要想锁不被释放,可以在这里进行catch,然后循环继续
}
}
}
public static void main(String[] args) {
SynchronizedException se = new SynchronizedException();
Runnable r = new Runnable() {
@Override
public void run() {
se.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r,"t2").start();
}
}
t1遇到异常释放了锁,t2立刻拿到了这把锁。参看下图的执行结果:
2.volatile关键字——使一个变量在多个线程中可见
♣ 例2.1 volatile的可见性
/**volatile关键字,使一个变量在多个线程中可见;但是volatile并不能保证多个线程共同修改running变量时所带来不一致的问题,即volatile不能完全代替synchronized
* A、B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果线程B修改了该变量,则线程A未必知道。
* 使用volatile关键字,会强制让所有的线程都去读变量的修改后的值。
*/
public class VolatileVisible {
volatile boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
void m() {
System.out.println("m start");
while(running) { //死循环。只有running=false时,才能执行后面的语句
}
System.out.println("m end");
}
public static void main(String[] args) {
VolatileVisible vt = new VolatileVisible();
new Thread(vt::m,"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
vt.running = false; //若running不被volatile关键字修饰时,线程“看不见”running被修改了
}
}
volatile 只保证原子性,而 synchronized 既保证可见性又保证原子性,但是synchronized太“重”了(效率很低)!
♣ 例2.2 volatile不具备原子性
/*10个线程分别执行10000次count++,count是对象vna的成员变量,按理来说最终count=100000,
但是最终每次执行结果都不一样,count一直小于100000,说明volatile不具备原子性*/
public class VolatileNoAtomic {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++) {
count ++ ;
}
}
public static void main(String[] args) {
VolatileNoAtomic vna = new VolatileNoAtomic();
List<Thread> threads = new ArrayList<>();
for (int i=0; i<10; i++) {
threads.add(new Thread(vna::m, "thread" + i));
}
threads.forEach(o->o.start());
threads.forEach((o)->{
try {
//join()方法阻塞调用此方法的线程,直到线程t完成,此线程再继续。通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
o.join(); //相当于在main线程中同步o线程,o执行完了,main线程才有执行的机会
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(vna.count);
}
}
... 每次结果都不一样,不是预期的100000。
♣ 例2.3 synchronized既保证可见性又保证了原子性
/*上例中,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性*/
public class SynVisibleAndAtomic {
int count = 0;
synchronized void m() { //m方法加了synchronized修饰,保证了原子性和可见性
for (int i=0; i<10000; i++) {
count ++ ;
}
}
public static void main(String[] args) {
SynVisibleAndAtomic sva = new SynVisibleAndAtomic();
List<Thread> threads = new ArrayList<>();
for (int i=0; i<10; i++) {
threads.add(new Thread( sva::m , "thread-" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)-> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(sva.count); //100000
}
}
♣例2.4 实现一个容器,提供两个方法add、size,线程1添加10个元素到容器,线程2监控元素的的个数,当个数为5个时,线程2提示并结束。
♣♣ 2.4.1 给容器list添加volatile
/*给list添加volatile之后,t2能够接到通知,但t2线程的死循环很浪费CPU*/
public class MyContainerVolatile {
volatile List list = new ArrayList();
public void add(Object o) { //add
list.add(o);
}
public int size() { //size
return list.size();
}
public static void main(String[] args) {
MyContainerVolatile mcv = new MyContainerVolatile();
new Thread( () -> { //该线程负责往list里添加
for (int i=0; i<10; i++) {
mcv.add(new Object());
System.out.print(" add-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread( () -> { //该线程一直监测list的size,直到size=5
while(true) { //一直监测着,很浪费CPU
if(mcv.size() == 5) { //此处未加同步,仍然可能会出现t1中又一次++为6了,才break
break;
}
}
System.out.print(" t2结束 ");
},"t2").start();
}
}
上面的代码能解决问题,但t2线程的死循环很浪费CPU,影响性能!更优方法请参看下例:
♣♣ 2.4.2 使用wait()和notify()做到,wait释放锁,而notify不会释放锁
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
当 notify/notifyAll() 被执行时,会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
/*wait会释放锁,notify则不会。t1中notify唤醒t2,本线程不会释放锁,会一直执行下去直至被wait或synchronized代码块结束*/
public class MyContainerWaitNotify {
volatile List list = new ArrayList();
public void add(Object o) {
list.add(o);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainerWaitNotify mcwn = new MyContainerWaitNotify();
final Object lock = new Object();
new Thread(()->{
synchronized (lock) {
System.out.print(" ***线程t2启动*** ");
if (mcwn.size() != 5) {
try {
lock.wait(); //size不等于5时,就一直在那等着,直到被t1叫醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(" ***线程t2结束*** ");
lock.notify(); //通知t1继续执行
}
}, "t2").start();
new Thread(()->{
synchronized (lock) {
for(int i=0; i<10; i++) {
mcwn.add(new Object());
System.out.print(" add-" + i);
if (mcwn.size() == 5) {
lock.notify(); //唤醒另一个线程t2,本线程继续执行,直至synchronized包裹的代码块结束或者调用了wait
try {
lock.wait(); //释放锁,让t2得以执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1").start();
}
}
上述使用wait和notify也解决了问题,但是有点过于复杂。java提供了门栓/门闩。
♣♣ 2.4.3 使用CountDownLatch(门闩)的await和countdown方法替代wait、notify方法来进行通知。
/*使用Latch(门栓)的await和countdown方法替代wait notify方法来进行通知。好处是通信方式简单,同时也可以指定等待时间。
* CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行.
* 当不涉及同步,只涉及锁定,用synchronized+wait/notify就显得太重了。这时应该考虑CountDownLatch/cyclicbarrier/semephore.*/
public class MyContainerLatch {
volatile List list = new ArrayList(); //添加volatile,使t2能够得到通知
public void add(Object o) {
list.add(o);
}
public int size() {
return list.size();
}
public static void main(String[] args) {
MyContainerLatch mcl = new MyContainerLatch();
CountDownLatch latch = new CountDownLatch(1); //当1变成0时,门就开了
new Thread(() -> {
System.out.print(" *t2启动* ");
if (mcl.size() != 5) {
try {
latch.await(); //等待不需要锁定一个对象
//latch.await(5000,TimeUnit.MILLISECONDS); //也可以指定等待时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(" *t2结束* ");
},"t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
System.out.print(" *t1启动* ");
for (int i=0; i<10; i++) {
mcl.add(new Object());
System.out.print(" add-" + i);
if (mcl.size() == 5) {
latch.countDown(); //打开门闩,让t2得以执行。调用一次countDown,就减1
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(" *t1结束* ");
},"t1").start();
}
}
3.原子变量类(AtomXxx)
原子变量类 (Atomics)是基于CAS实现的能够保障对共享变量进行read-modify-write更新操作的原子性和可见性的一组工具类。所谓的read-modify-write更新操作,是指对共享变量的更新不是一个简单的赋值操作,而是变量的新值依赖于变量的旧值,例如自增操作 “count++”。由于volatile无法保障自增操作的原子性,而原子变量类的内部实现通常借助一个volatile变量并保障对该变量的read-modify-write更新操作的原子性,因此它可以被看作增强型的volatile变量。原子变量类一共有12个,可以被分为4组:
AtomicXxx类本身是原子性的,但是不能保证多个方法连续调用是原子性的!在 java.util.concurrent.atomic 包中。
♣ 例3.1 原子性操作int类型——AtomicInteger
public class AtomicIntegerTest {
AtomicInteger count = new AtomicInteger(0);
void m() {
for (int i=0; i<10000; i++) {
count.incrementAndGet(); incrementAndGet()-先+1,再返回; getAndIncrement()-先返回,再+1
}
}
public static void main(String[] args) {
AtomicIntegerTest ait = new AtomicIntegerTest();
List<Thread> threads = new ArrayList<>();
for (int i=0; i<10; i++) {
threads.add(new Thread( ait::m , "thread" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(ait.count); //100000
}
}
4.ReentrantLock-重入锁
ReentrantLock重入锁,是实现Lock接口的一个类。在Java中通常实现锁有两种方式,一种是synchronized关键字,另一种是Lock。(synchronized在jdk1.5之后做了优化,性能提升了很多,只是使用ReentrantLock更灵活一些)。
tips | synchronized(关键字) | Lock(接口) |
---|---|---|
实现 | 基于JVM层面实现(JVM控制锁的获取和释放) | 基于JDK层面实现(我们可以借助JDK源码理解) |
使用 | 不用我们手动释放锁 | 需要手动上锁和释放锁(finally中unlock) |
锁获取超时 | 不支持。拿不到锁就一直在那等着,等到“死”。 | 支持。可以设置超时时间,时间过了没拿到就放弃,即Lock可以知道线程有没有拿到锁。 |
获取锁响应中断 | 不支持。 | 支持。可以设置是否可以被打断。 |
释放锁的条件 | 满足一个即可:①占有锁的线程执行完毕②占有锁的线程异常退出③占有锁的线程进入waiting状态释放锁 | 调用unlock()方法 |
公平与否 | 非公平锁。(公平指的是哪个线程等的时间长就把锁交给谁) | 默认为非公平锁,可以设置为公平锁(排队等候)。 |
♣ 例4.1 ReentrantLock可以用来代替synchronized
/* ReentrantLock用来替代synchronized
* ReentrantLock必须要手动释放锁。使用synchronized锁定如果遇到异常,jvm会自动释放锁,但是Lock必须手动释放,因此常常在finally中释放锁*/
public class ReentrantLockTest1 {
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock(); //加锁 //相当于synchronized(this)
for (int i=0; i<10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.print(" " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放锁
}
}
void m2() {
lock.lock(); //加锁
System.out.print(" m2()... ");
lock.unlock(); //释放锁
}
public static void main(String[] args) {
ReentrantLockTest1 r1 = new ReentrantLockTest1();
new Thread(r1::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r1::m2).start();
}
}
♣ 例4.2 ReentrantLock可以进行尝试锁定tryLock()
/*使用ReentrantLock可以进行尝试锁定tryLock();若无法锁定或在指定时间内无法锁定,线程可以决定是否等待*/
public class ReentrantLockTryLock {
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock();
for (int i=0; i<10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.print(" " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 使用tryLock进行尝试锁定,不管锁定与否,方法都将会继续执行,可以根据tryLock的返回值判定是否被锁定了
* 可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unlock的处理,必须放到finally中。
*/
void m2() {
/* boolean locked = lock.tryLock();
System.out.print(" m2..." + locked + " ");
if (locked) lock.unlock(); //false */ //不指定尝试时间
boolean locked = false;
try {
locked = lock.tryLock(5,TimeUnit.SECONDS); //指定超时时间为5s
System.out.println(" m2..." + locked + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockTryLock r1 = new ReentrantLockTryLock();
new Thread(r1::m1).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r1::m2).start();
}
}
♣ 例4.3 ReentrantLock可调用lockInterruptibly方法,对线程的interrupt方法作出响应,在一个线程等待的过程中,可以被打断。
/*ReentrantLock可调用lockInterruptibly()方法,对线程的interrupt()方法作出响应,在一个线程等待的过程中,可以被打断。
* ReentrantLock的lock()方法是不能被打断的,即锁用lock()方法锁定,线程调用interrupt()方法是毫无作用的*/
public class ReentrantLockInterruptibly {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
lock.lock();
System.out.print(" t1 start... ");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); //t1不停的运行,睡死了
System.out.print(" t1 end... ");
} catch (InterruptedException e) {
System.out.print(" t1-interrupted! ");
} finally {
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
// lock.lock(); //不能对interrupt()方法作出响应
lock.lockInterruptibly(); //也是上锁,但是可以对interrupt()方法作出响应
System.out.print(" t2 start... ");
TimeUnit.SECONDS.sleep(5);
System.out.print(" t2 end... ");
} catch (InterruptedException e) {
System.out.println(" t2-interrupted! ");
} finally {
lock.unlock();
}
});
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt(); //打断t2的等待
}
}
♣ 例4.4 ReentrantLock可以指定为共享锁
/*ReentrantLock可以指定为公平锁,构造方法中将fair属性设置为true即为公平锁,fair默认为false*/
public class ReentrantLockFair extends Thread {
private static ReentrantLock lock = new ReentrantLock(true); //参数为true表示为公平锁,可对比输出结果
@Override
public void run() {
for (int i=0; i<20; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "-获得锁 ");
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockFair r1 = new ReentrantLockFair();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
}
}
fair设置为true时,两个线程交替执行,如下图; fair设置为false时,执行顺序是随机的,并不是谁等的时间长谁执行。
♣ 例4.5 写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。
♣♣ 4.5.1 使用wait和notify/notifyAll()来实现
/*写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。
* wait和notifyAll方法来实习。*/
public class ProducerCustomerWaitNotifyAll {
final private LinkedList<Object> list = new LinkedList<Object>();
final private int MAX = 10; //最多十个元素
private int count = 0;
public synchronized void put(Object t) {
while (list.size() == MAX) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(t);
++count;
this.notifyAll(); //通知消费者进程进行消费
//notify只叫醒一个,叫醒的可能还是生产者,所有线程一直wait,程序就卡死了,所以用notifyAll
}
public synchronized Object get() {
Object t = null;
while (list.size() == 0) { //
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = list.removeFirst();
count --;
this.notifyAll(); //通知生产者进程进行生产
return t;
}
public static void main(String[] args) {
ProducerCustomerWaitNotifyAll pc = new ProducerCustomerWaitNotifyAll();
for (int i=0; i<10; i++) { //10个消费者
new Thread(() -> {
for (int j=0; j<5; j++) { //每个消费者最多消费5个
System.out.println(pc.get());
}
},"c"+i).start();
}
//启动生产者线程
for (int i=0; i<2; i++) { //2个生产者
new Thread(() -> {
for (int j=0; j<25; j++) { //每个生产者最多生产25个
pc.put(Thread.currentThread().getName() + " " + j);
}
},"p"+i).start();
}
}
}
♣♣ 4.5.1 使用Lock和Condition来实现
/*使用Lock和Condition来实现生产者和消费者的同步容器,
相比使用wait/notifyAll,使用Conditionde的方式能更加精确地指定哪些线程被唤醒。*/
public class ProducerConsumerLockCondition {
final private LinkedList<Object> list = new LinkedList<>();
final private int MAX = 10;
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put(Object obj) {
try {
lock.lock();
while (list.size() == MAX) {
producer.await();
}
list.add(obj);
++count ;
consumer.signalAll(); //通知消费者进行消费
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public Object get() {
Object obj = null;
try {
lock.lock();
while (count == 0) {
consumer.await();
}
obj = list.removeFirst();
count -- ;
producer.signalAll(); //通知生产者进行生产
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return obj;
}
public static void main(String[] args) {
ProducerConsumerLockCondition c = new ProducerConsumerLockCondition();
for (int i=0; i<10; i++) {
new Thread(()->{
for (int j=0; j<5; j++) {
System.out.println(c.get());
}
},"c"+i).start();
}
//启动生产者线程
for (int i=0; i<2; i++) { //2个生产者
new Thread(() -> {
for (int j=0; j<25; j++) { //每个生产者最多生产25个
c.put(Thread.currentThread().getName() + " " + j);
}
},"p"+i).start();
}
}
}
5.ThreadLocal 线程局部变量
/*ThreadLocal是使用空间换时间,synchronized是使用时间换空间。
* 比如在Hibernate中的session就存在于ThreadLocal中,避免Synchronized的使用
* 线程局部变量属于每个线程都有自己的,线程间不共享,互不影响*/
public class ThreadLocalTest {
static ThreadLocal<Person> tL = new ThreadLocal<>(); //每个线程的tL互不影响
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tL.get());
}).start();
new Thread(()-> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
tL.set(new Person());
}).start();
}
static class Person {
String name = "zhangsan";
}
}
第二个线程设置了值,但是第一个线程get得到的是:,说明线程局部变量是互不影响的!
补充:
常用的并发工具类:
闭锁:CountDownLatch
闭锁允许一个线程或多个线程等待特定情况,同步完成线程中其他任务。
栅栏:CyclicBarrier
CyclicBarrier和CountDownLatch都可以协同多个线程,让指定数量的线程等待期他所有的线程都满足某些条件之后才继续执行。CyclicBarrier可以重复使用(reset),而CountDownLatch只能够使用一次,如果还需要使用,必须重现new一个CountDownLatch对象。构造方法CyclicBarrier(int, Runnable) 所有线程达到屏障后,执行Runnable。
信号量:Semaphore
信号量用来控制同时访问特定资源的线程数量。
交换者:Exchanger
Exchanger 交换者用于在两个线程之间传输数据,被调用后等待另一个线程达到交换点,然后相互交互数据。
常用的并发容器(下一篇会详细讲到):
ConcurrentHashMap:JDK1.7实现:分段锁;JDK1.8实现:元素(key)锁+链表+红黑树
SkipList:跳表自动随机维护一套索引,用于高效的索引List中的有序数据。
ConcurrentSkipListMap:TreeMap的并发实现
ConcurrentSkipListSet:TreeSet的并发实现
ConcurrentLinkedQueue:LinkedList的并发实现
CopyOnWriteArrayList:写时复制,在添加元素是,复制一个新的容器,在新容器中新增元素;读数据都在Old容器中操作,进行读写分离。数据一致性较弱,适合读多写少的场景。
CopyOnWriteArraySet:同上