本篇主要总结同步器的相关例子:包括synchronized、volatile、原子变量类(AtomicXxx)、CountDownLatch、ReentrantLock和ThreadLocal。还涉及到wait和notify/notifyAll。以及一些面试题如:生产者、消费者问题
回忆关于线程的几个基本知识点:
- 线程的概念(程序中不同的执行路径可以放到不同的CPU中同步运行);
- 如何启动一个线程(继承Thread类 / 实现Runnable接口 / 实现Callable接口,,,调用start()方法);
- 基本的线程同步方式 (synchronized——锁定的是一个对象、...)
对于synchronized的理解:
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会锁住当前对象而保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住class对象。
1.对某个对象加锁
♣ 例1.1:new一个对象作为锁
public class SyncronizeTest {
private static int count=0;
private Object lock= new Object();
public void m(){
synchronized (lock){
count++;
System.out.println(Thread.currentThread().getName()+"-----"+count);
}
}
}
例1 中,这把锁是自己new的,每次new出一个毫无其他功能的对象就当锁的对象比较麻烦。所以可以用synchronized(this)。
♣ 例1.2:锁定自身对象
如下两种写法其实效果都是一样的,都是锁住的当前对象。
public class SyncronizeTest {
private static int count=0;
public void m1(){
synchronized (this){
count++;
System.out.println(Thread.currentThread().getName()+"-----"+count);
}
}
//m1()等同于下面的m2(),都是锁住的当前对象。
public synchronized void m2(){
count++;
System.out.println(Thread.currentThread().getName()+"-----"+count);
}
}
}
例1.3:synchronized用在静态方法上
public class SyncronizeTest {
private static int count=0;
public static synchronized void m1(){
count++;
System.out.println(Thread.currentThread().getName()+"-----"+count);
}
//m1()和 m2()效果也是一样的,所以当synchronized锁定一个静态方法时,锁定的是当前类的class对象
public static void m2(){
synchronized (SyncronizeTest.class){
count++;
System.out.println(Thread.currentThread().getName()+"-----"+count);
}
}
}
例1.4:锁住线程的run方法
public class SyncronizeTest implements Runnable{
private int count=10;
@Override
public synchronized void run() {
count--;
System.out.println(Thread.currentThread().getName()+"---"+count);
}
public static void main(String[] args) {
SyncronizeTest t = new SyncronizeTest();
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.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:出现异常,默认情况下锁会被释放,
程序执行过程中,如果出现异常,默认情况锁会被释放,所以在并发处理的过程中,有异常要多加小心,不然会发生不一致的情况;
public class SyncronizedException {
int count=5;
synchronized void m(){
System.out.println(Thread.currentThread().getName()+"--"+"start");
while(true){
count--;
System.out.println(Thread.currentThread().getName()+"--"+count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count==3){
int i=count/0; //t1线程先启动,当count--等于3时,线程报错释放锁。t2拿到锁
}
if(count<=0){
break;
}
}
}
public static void main(String[] args) {
SyncronizedException s= new SyncronizedException();
Runnable runnable= new Runnable() {
@Override
public void run() {
s.m();
}
};
new Thread(runnable,"t1").start();
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(runnable,"t2").start();
}
}
t1遇到异常释放了锁,t2立刻拿到了这把锁继续执行。运行结果为:
2.volatile关键字——使一个变量在多个线程中可见
♣ 例2.1 volatile的可见性
public class volatileTest {
/*volatile*/ boolean running =true;
void m(){
System.out.println("m start");
while(running){
}
System.out.println("m end");
}
public static void main(String[] args) {
volatileTest v=new volatileTest();
new Thread(v::m,"t1").start();
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
v.running=false;
}
}
输入结果:m start
这里可以看到虽然后面将running=false,但是并没有生效,m end并没有输出。
将/*volatile*/释放之后打印结果:
m start
m end
很明显可以看到加了volatile之后线程里面值修改是有效的,所以才会输出m end。
那为什么这样加了volatile就不同了呢?首先这里需要我们看看JMM (java 内存模型)。
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝。每次线程先去主内存中拿值放到自己的缓存中,线程对变量的所有操作都必须在自己的缓存中进行,而不能直接读写主内存中的变量。
细节流程图:
大概了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来讲,比如我们上文中的running,主线程将其修改为false这个动作发生在主线程中,线程t1并没有并没有读取到主内存中的running'值更新,所以就导致了上述的问题。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,有点炮打蚊子的意思。比较合理的方式其实就是volatile,效率比synchronized高太多。
volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操作会导致其他线程中的缓存无效。
所以加了valatile之后,线程会将变量的最新值同步给线程的缓存中。
例2.2 volatile不具备原子性
但是需要注意的是,我们一直在拿volatile和synchronized做对比,仅仅是因为这两个关键字在某些内存语义上有共通之处,volatile并不能完全替代synchronized,它依然是个轻量级锁,在很多场景下,volatile并不能胜任。看下这个例子:
public class volatileTest {
volatile int num = 0;
void m(){
for (int i = 0; i <10000 ; i++) {
num++;
}
}
public static void main(String []args) throws InterruptedException {
volatileTest v= new volatileTest();
List<Thread> threads= new ArrayList<>();
for (int i = 0; i <10 ; i++) {
threads.add(new Thread(v::m,"thread"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
}catch (InterruptedException e){
e.printStackTrace();
}
});
System.out.println(v.num);
}
}
输出结果:39572 每次运行输出结果都不一致。总是小于100000的。
针对这个示例,一些同学可能会觉得疑惑,如果用volatile修饰的共享变量可以保证可见性,那么结果不应该是100000么?
问题就出在num++这个操作上,因为num++不是个原子性的操作,而是个复合操作。我们可以简单讲这个操作理解为由这三步组成:
1.读取
2.加一
3.赋值
所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于100000。
♣ 例2.3 synchronized既保证可见性又保证了原子性
再上面的m()上加上同步锁就可以保证原子性了。
public class volatileTest {
volatile int num = 0;
synchronized void m(){
for (int i = 0; i <10000 ; i++) {
num++;
}
}
}
♣例2.4 实现一个容器,提供两个方法add、size,线程1添加10个元素到容器,线程2监控元素的的个数,当个数为5个时,线程2提示并结束。
方法1:使用valatile完成。对容器元素个数严格监控,缺点浪费资源。
/***
* t2必须要先启动,检测list.size==5,使用while循环判断。浪费了cpu
*/
public class watchSize {
static volatile List<Object> list= new ArrayList<>();
public void add(Object o){
list.add(o);
}
public Integer size(){
return list.size();
}
public static void main(String[] args) {
watchSize w = new watchSize();
new Thread(()->{
System.out.println("t2启动鸟");
while(true){
if(list.size()==5){
break;
}
}
System.out.println("t2结束鸟");
},"t2").start();
new Thread(()->{
System.out.println("t1启动鸟");
for (int i = 1; i <=10 ; i++) {
list.add(1);
System.out.println(" add--" + i);
}
System.out.println("t1结束鸟");
},"t1").start();
}
}
方法1:使用wait/notify完成。同样先启动t2,如果list.size=5,t2.wait()等待,直到t1往容器中添加元素个数到5时唤醒t2。
public class watchSize {
static volatile List<Object> list= new ArrayList<>();
public void add(Object o){
list.add(o);
}
public Integer size(){
return list.size();
}
public static void main(String[] args) {
watchSize w = new watchSize();
final Object lock = new Object();
new Thread(()-> {
System.out.println("t2启动鸟"); //t2先启动
synchronized (lock) {
if (list.size() != 5) { //监测size不等于5
try {
lock.wait(); //wait()释放锁给t1,直到t1调用notyfy()重新拿到锁执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("t2结束鸟");
lock.notify(); //t2调用notify唤醒t1,直到t2线程执行结束,t1拿到锁
},"t2").start();
new Thread(()->{
System.out.println("t1启动鸟");
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add(1);
System.out.println(" add--" + i);
if(list.size()==5){
lock.notify(); //t1添加元素个数5时,notify()线程t2,并要释放锁给t1
try {
lock.wait(); //因为notify()不释放锁,所以只能用wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
System.out.println("t1结束鸟");
},"t1").start();
}
}
方法二程序性能好很多但是略微复杂。其实就是使用了wait和notyfy方法来控制锁。这里首先要理解wait()是释放了锁的,但是notyfy()没有释放锁。所以需要注意。
方法三:使用门闩接。对于方法二的优化,方法二之所以复杂就是因为控制锁的时候需要两个线程协调锁。所以要程序更简单使用一个控制锁的flag就好,这里使用门栓CountDownLatch。CountDownLatch中值1-->0的时候们拴就开了,countDown()方法开拴。仍然是上面的思路,t2等待,t1添加元素到5给t2执行。
public class watchSize{
static volatile List<Object> list= new ArrayList<>();
public void add(Object o){
list.add(o);
}
public Integer size(){
return list.size();
}
public static void main(String[] args) {
watchSize w = new watchSize();
CountDownLatch latch = new CountDownLatch(1);
new Thread(()->{
System.out.println("t2启动鸟");
if (list.size() != 5) {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2结束鸟");
},"t2").start();
new Thread(()->{
System.out.println("t1启动鸟");
for (int i = 1; i <= 10; i++) {
w.add(new Object());
System.out.println("add"+i);
if(w.size()==5){
latch.countDown();
}
}
System.out.println("t1结束鸟");
},"t1").start();
}
}
3.原子变量类(AtomXxx)
之前谈到使用volatile关键字解决num++操作的原子性问题,只有可见性。那么这类简单的计算有没有更好的方式操作呢?答案肯定是有的。 针对num++这类复合类的操作,可以使用java并发包中的原子操作类。原子操作类是通过循环CAS的方式来保证其原子性的。看下面代码。简单的变量保证原子性和可见性可以使用并法宝里面的原子操作类完成。更加复杂的就要用到同步锁或者lock了
原子变量类 (Atomics)是基于CAS实现的能够保障对共享变量进行read-modify-write更新操作的原子性和可见性的一组工具类。所谓的read-modify-write更新操作,是指对共享变量的更新不是一个简单的赋值操作,而是变量的新值依赖于变量的旧值,例如自增操作 “count++”。由于volatile无法保障自增操作的原子性,而原子变量类的内部实现通常借助一个volatile变量并保障对该变量的read-modify-write更新操作的原子性,因此它可以被看作增强型的volatile变量。原子变量类一共有12个,可以被分为4组:
来看下之前谈到使用volatile关键字解决num++操作的原子性问题,现在用AtomicInteger 完成,更简单。
volatile AtomicInteger num = new AtomicInteger(0);;
void m(){
for (int i = 0; i <10000 ; i++) {
num.incrementAndGet();//原子性的num++,通过循环CAS方式
}
}
4.ReentrantLock-重入锁
ReentrantLock重入锁,是实现Lock接口的一个类。在Java中通常实现锁有两种方式,一种是synchronized关键字,另一种是Lock。(synchronized在jdk1.5之后做了优化,性能提升了很多,只是使用ReentrantLock更灵活一些)。
synchronized和Lock的区别
synchronized(关键字) | Lock(接口) | |
实现 | 基于JVM层面实现(JVM控制锁的获取和释放) | 基于JDK层面实现(我们可以借助JDK源码理解) |
使用 | 不用我们手动释放锁 | 需要手动上锁和释放锁(finally中unlock) |
锁获取超时 | 不支持。拿不到锁就一直在那死等 | 支持。可以设置超时时间,时间过了没拿到就放弃,即Lock可以知道线程有没有拿到锁。 |
获取锁响应中断 | 不支持。 | 支持。可以设置是否可以被打断。 |
释放锁的条件 | 满足一个即可:①占有锁的线程执行完毕②占有锁的线程异常退出③占有锁的线程进入waiting状态释放锁 | |
公平与否 |
♣ 例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();
}
}
必须等到线程m1执行完毕释放锁了之后,线程m2才能执行。
♣ 例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可以指定为共享锁(公平锁)
public class ReentrantLockFair {
private static ReentrantLock lock=new ReentrantLock(true);//设置共享锁
public void run(){
for (int i = 0; i < 5 ; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockFair r=new ReentrantLockFair();
new Thread(()->{
r.run();
},"r1").start();
new Thread(()->{
r.run();
},"r2").start();
}
}
ReentrantLock设置为true时,两个线程交替执行,如下图;fair设置为false时,执行顺序是随机的,并不是谁等的时间长谁执行。
♣ 例4.5 写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。
思路:需要开启两个线程,一个生产,一个消费。同步容器里面元素个数最多10个,设置一个MAX。另外生产者在put元素时需要先判断容器里面是否已满,同样消费者在消费的时先判断容器是否为空。注意的是,判断的时候要用while而不是if。一般来说effective JAVA 讲到wait的时候 有个说法99%的情况结合while使用,而不是if。
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 o){
while(list.size()==MAX){ //特别注意一下这里是while而不是if
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(o);
this.notifyAll();
++count;
}
public synchronized Object get(){
while(list.size()==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object o = list.removeFirst();
this.notifyAll();
--count;
return o;
}
public static void main(String[] args) {
ProducerCustomerWaitNotifyAll p = new ProducerCustomerWaitNotifyAll();
for (int i=0; i<10; i++) { //10个消费者
new Thread(() -> {
for (int j=0; j<5; j++) { //每个消费者最多消费5个
System.out.println(p.get());
}
},"c").start();
}
for (int i=0; i<2; i++) { //2个生产者
new Thread(() -> {
for (int j=0; j<30; j++) { //每个生产者最多生产30个
p.put(Thread.currentThread().getName() + " " + j);
}
},"p").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";
}
}
运行结果如下:
线程局部变量互不影响的!也可以把ThreadLocal理解为一个map,每个线程在调度的时候就创建了一个自身<id,thread>的map,各个线程调用不会影响别的线程ThreadLocal的变量。
补充:
常用的并发工具类:
闭锁: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:同上