volatile关键字同步
先来一段程序,对比,有volatile和没有volatile的区别:
1.没有使用volatile
下面执行的效果:执行一直在进行,做死循环,说明vt中修改的flag值没有在主线程中读到。
public class VolatileTest {
public static void main(String[] args) {
VolatileThread vt=new VolatileThread();
//创建一个子线程
new Thread(vt,"子线程").start();
//主线程中写一个循环
while(true) {
if(vt.isFlag()) {
System.out.println("子线程中把flag给改成了true,主线程这里读到新值!");
break;
}
}
}
}
class VolatileThread implements Runnable{
private boolean flag=false; //共享变量
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//子线程执行体
try {
Thread.sleep(1000); //放大同步问题的发生
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag=true;
System.out.println("子线程修改了flag标志值:改成了true");
}
}
-----------------------------------------------------------------------
子线程修改了flag标志值:改成了true
| //卡在这了,不过不会卡很久,可以在条件语句块,加一个输出就知道了,这时因为子线程还在抢资源等待,一时抢到资源,就会更新flag标记的
2.共享变量上用了voliatle关键字后,就可以在主线程中读到子线程修改后的值
.....
private volatile boolean flag=false; //共享变量
.....
-------------------------------------------------------------------------
子线程中把flag给改成了true,主线程这里读到新值!
子线程修改了flag标志值:改成了true
//这里不会卡,是因为子线程一改就让其它线程看到了(包括主线程-它也是线程吗,不要紧想了)
即使我们在子线程中将线程的共享变量flag的值修改成了false,
但是主线程在while条件判断的时候读到的flag一直是false,这是什么原因导致的呢?
这就涉及到内存的可见性问题了,在讲怎么解决内存可见性问题之前,什么是内存可见性。
内存可见性其实就是共享变量在线程间的可见性
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
可见性:一个线程对共享变量值的修改,能够及时的被其他线程看到(包括主线程)
Java线程内存模型
所有的变量都存储在主内存中(操作系统给进程分配的内存空间)
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本
线程对共享变量的所有操作都必须在自己的工作内存(working memory,是cache和寄存器的一个抽象,而并不是内存中的某个部分)不同线程之间,当前线程无法直接访问其他线程的工作内存中的变量,线程间变量值得传递需要通过主内存来完成
共享变量可见性的实现原理
把工作内存1中更新过的共享变量刷新到主内存中
将主内存中最新的共享变量的值更新到工作内存2中
实质上共享变量在线程间的常常看不见,要解决这个问题,方式很多:
volatile关键字实现可见性,尤其是在嵌入式开发中,这个关键字在多线程开发不可或缺
前面学习的synchronized实现可见性
JMM关于synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
while(true){
synchronized (vt){
if(td.isFlag()){
System.out.println("------------------");
break;
}
}
}
缺点:程序效率低,对计算机硬件资源是高开销动作。
synchronized和volatile比较
volatile不需要加锁,比synchronized更轻量级,不会阻塞线程,效率更高从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁
volatile不具备“互斥性”,synchronized就具备“互斥性”
何为互斥性?
比方说当我们用synchronize修饰方法,
当一个线程抢到锁执行该方法后另一个线程无法再抢到锁执行该方法
synchronized既能保证可见性,又能保证原子性,
而volatile只能保证可见性,不能保证原子性。原子性下部分详细来讲!
如果能用volatile解决问题,还是应尽量使用volatile,因为它的效率更高
理解volatile不能保证原子性
什么是原子性,什么是原子性操作?
A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:
- 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
- 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。
我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。
***例:
public class VolatileNotAtomicTest {
public static void main(String[] args) {
VolatileThread2 volatileThread2=new VolatileThread2();
for(int i=0;i<10;i++) {
new Thread(volatileThread2,"线程"+i).start();
}
}
}
class VolatileThread2 implements Runnable{
private volatile int serialNum=0;
public int getSerialNum() {
return serialNum++;
}
@Override
public void run() {
try {
Thread.sleep(500); //放大问题的发生
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("显录serialNum完后,然后它自增1: "+getSerialNum());
}
}
------------------------------------------------------------------------
显录serialNum完后,然后它自增1: 0
显录serialNum完后,然后它自增1: 1
显录serialNum完后,然后它自增1: 2
显录serialNum完后,然后它自增1: 3
显录serialNum完后,然后它自增1: 4 //重复了,原子型出问题了
显录serialNum完后,然后它自增1: 4
显录serialNum完后,然后它自增1: 6
显录serialNum完后,然后它自增1: 5
显录serialNum完后,然后它自增1: 7
显录serialNum完后,然后它自增1: 8
共享变量加了volatile修饰,虽有同步作用,但在特殊情况下,不能保证原子性,其没有如sychnorized那样的互斥作用,它没锁机制*
自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
假如某个时刻变量serialNumber的值为10,
线程1对变量进行自增操作,线程1先读取了变量serialNumber的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量serialNumber的原始值,
线程2会直接去主存读取serialNumber的值(volatile就是要直接从内存读取),
发现serialNumber的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
回到线程1接着进行加1操作,由于已经读取了serialNumber的值,
注意此时在线程1的工作内存中serialNumber的值仍然为10,
所以线程1对serialNumber进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的,不具有互斥性。
问题怎么解决呢?就要用到JUC了!
JUC这个工具包里,有一个包含原子变量子包,java.util.concurrent.atomic,里边封装了一系列常用的数据类型对应的封装类,Java.util.concurrent.atomic中实现的原子操作类包括:
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference
这些类都保证了两点:
1、类里的变量都用了volatile保证内存是可见的
2、使用了一个算法CAS,保证对这些数据的操作具有原子性
具体的代码上的修改:
.....
//private volatile int serialNum=0;
private AtomicInteger serialNumber = new AtomicInteger (0);
public int getSerialNum() {
//return serialNum++;
return serialNumber .getAndIncrement();
}
....
-------------------------------------------------------------------------------
显录serialNum完后,然后它自增1: 4
显录serialNum完后,然后它自增1: 5
显录serialNum完后,然后它自增1: 3
显录serialNum完后,然后它自增1: 1
显录serialNum完后,然后它自增1: 2
显录serialNum完后,然后它自增1: 0
显录serialNum完后,然后它自增1: 9
显录serialNum完后,然后它自增1: 6
显录serialNum完后,然后它自增1: 7
显录serialNum完后,然后它自增1: 8 //没有重复的了
synchronized,Lock,Volatitle ,JUC包工具比较选择
上面的代码就效率上讲,比synchronized和Lock实现的线程同步都高,同时既保证了内存可见性,有保证了操作原子性,也解决了线程安全问题,是一种无锁的线程同步。
共享变量加了volatile修饰,虽有同步作用,但在特殊情况下,不能保证原子性,其没有如sychnorized那样的互斥作用,它没锁机制。
如果能用volatile,JUC解决问题,还是应尽量使用volatile和JUC包工具类,因为它们的效率更高,JUC工具包类也是用了volatile关键字保证同步,再通过CAS算法保证原子型。
CAS算法逻辑的理解
通过申明一个volatile类型的变量,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。
1、什么是CAS?
CAS:Compare and Swap,即比较再交换。
乐观锁和悲观锁
传统的锁机制,例如 java 的 synchronized 关键字,他代表了 java 中悲观锁技术,保证了某一时刻仅有一个线程能访问同步代码/方法。
synchronized 能够很好地工作,却有着 (相对) 比较大的性能开销。
乐观锁 (相对悲观锁) 对性能会有很大的帮助。
他的核心思想是:你寄希望于在没有冲突的情况下完成一次更新操作,
使用乐观锁技术更新时会进行 “冲突检测” 来判断是否有其他的线程干扰,若是 (有其他线程干扰) 则视本次更新操作失败,一般会进行重试。
Compare and Swap 就是典型的乐观锁技术。
2、CAS算法理解
CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。这一比较并交换的操作是原子的,不可以被中断。初一看,CAS也包含了读取、比较 (这也是种操作)和写入这三个操作,和之前的i++并没有太大区别,的确在操作上没有区别,但CAS是通过硬件命令保证了原子性,而i++没有,且硬件级别的原子性比i++这样高级语言的软件级别的运行速度要快地多。虽然CAS也包含了多个操作,但其的运算是固定的(就是个比较),这样的锁定性能开销很小。
简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。
t1,t2线程是同时更新同一变量56的值
因为t1和t2线程都同时去访问同一变量值56,
所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。
假设t1在与t2线程竞争中,线程t1竞争到了,能去更新变量的值,t2线程没争到,会等待不会死。要死是完成任务,或中断等操作。
注意:
失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试
t1线程去更新变量值改为57,然后写到内存中。*
此时对于t2来讲它会检测到,内存值变为了57,与预期值56不一致,就不能把它修改的值写回主内存,就会判定就操作失败了(想改的值不再是原来的值,因为很明显,有其它操作先改变了这个值)
总结CAS的核心逻辑:
就是指当V和A两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。这样子,就能保证操作的原子性了。
JUC工具包的工具类同步线程
JUC里的同步容器(集合)类
在不使用JUC的情况下,怎么解决这些非线程安全的集合的线程安全问题呢?
以前都是使用的Collections.synchronizedXXX()方法来转换!
List<String> li = Collections.synchronizedList(new ArrayList<>());
Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
当转换HashMap的时候Collections.synchronizedMap(new HashMap<>()),还可以直接使用HashTable
HashMap和HashTable的数据结构都是一样的Key—Value,所以可以替换,一个线程不安全,一个现在安全。
JUC里给我们提供了一系列同步容器类,用来解决非线程安全的集合类,我们只需要在多线程并发编程中,用这个类替换掉原来的HashMap,ArrayList,HashSet集合就可以了保证即是线程安全的,比使用Collections.synchronizedXXX()的效率高。
JUC里的ConcurrentHashMap优于同步的HashMap
JUC里的ConcurrentSkipListMap优于同步的TreeMap
JUC里的CopyOnWriteArrayList优于同步的ArrayList
JUC里的CopyOnWriteArraySet优于同步的ArraySet
线程安全(Thread-safe)的集合对象:
Vector 线程安全:
HashTable 线程安全:
StringBuffer 线程安全:
非线程安全的集合对象:
ArrayList :
LinkedList:
HashMap:
HashSet:
TreeMap:
TreeSet:
StringBulider:
例:演示用传统的线程安全的集合变量方法,会发生并发的复合操作异常
public class CollectionTest {
public static void main(String[] args) {
CollectionsThread ct =new CollectionsThread();
new Thread(ct,"1").start();
new Thread(ct,"1").start();
new Thread(ct,"1").start();
new Thread(ct,"1").start();
}
}
class CollectionsThread implements Runnable{
//线程共享变量,synchronizedList是保证集合线程安全的,这个会发生异常
private static List<String> list=Collections.synchronizedList(new ArrayList<>());
static {
list.add("aaa");
list.add("bbb");
list.add("ccc");
}
@Override
public void run() {
Iterator<String> it =list.iterator();
while(it.hasNext()) {
System.out.println(it.next());
//上面读,下面马上写,这样一边读一边写的操作就是集合的复合操作,这种操作线程多了会有写入并发异常发生
list.add("xiong");
}
}
}
-----------------------------------------------------------------------------
aaaException in thread "1" Exception in thread "1" Exception in thread "1" Exception in thread "1"
aaa
aaa
aaa
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
解决问题:用JUC容器类的集合类代替一下即可
//线程共享变量,synchronizedList是保证集合线程安全的,这个会发生异常
//private static List<String> list=Collections.synchronizedList(new ArrayList<>());
//JUC包里的容器类来处理集合的异常问题
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
但是最后还是要注意:使用CopyOnWriteArrayList的场景最好在使用Iterator,且写操作少的环境中,
因为写的多了,会降低程序的效率,因为他CopyOnWrite每次写的时候都会复制一个备份,增大开销。
CountDownLatch同步一批线程
JAVA并发包中有三个类用于同步一批线程的行为,
分别是CountDownLatch、Semaphore和CyclicBarrier。
CountDownLatch
CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,
即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。
CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。
调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法,每次调用countDown方法,
计数器的值减1。当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。
例:下面的代码主线程显示执行总时间的打印一定在最后显示
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch latch=new CountDownLatch(50);
CountDownLatchThread ctld= new CountDownLatchThread(latch);
long start = System.currentTimeMillis();
for(int i=1;i<51;i++) {
new Thread(ctld,"线程"+i).start();
}
try {
latch.await(); //阻塞主线程,直到初化50的计数器倒计时为0时
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("50个子线程花了多长时间:"+(end-start)/1000+"秒");
}
}
class CountDownLatchThread implements Runnable{
private CountDownLatch latch;
public CountDownLatchThread(CountDownLatch latch) {
this.latch=latch;
}
@Override
public void run() {
try {
for(int i=0;i<50000;i++) {
if(i%2==0) {
System.out.println(Thread.currentThread().getName()+"打印0-50000直接的偶数:"+i);
}
}
}finally {
latch.countDown(); //让latch里的计数器减1
}
}
}
------------------------------------------------------------------------------------
。。。。。
线程16打印0-50000直接的偶数:49992
线程16打印0-50000直接的偶数:49994
线程16打印0-50000直接的偶数:49996
线程16打印0-50000直接的偶数:49998
50个子线程花了多长时间:9秒
例:如果没有CountDownLatch对象作用,而了Thread.sleep(xxx),则如果没有足够的挂起时,则显示总花费时间不知道在那儿显示,如果有足够的时间,又会浪费时间,下面不该9秒可打印,但为了让在最后显示,我Thread.sleep(15000),挂起主线程15秒,浪费了6秒之多
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatchThread ctld= new CountDownLatchThread();
long start = System.currentTimeMillis();
for(int i=1;i<51;i++) {
new Thread(ctld,"线程"+i).start();
}
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("50个子线程花了多长时间:"+(end-start)/1000+"秒");
}
}
class CountDownLatchThread implements Runnable{
@Override
public void run() {
try {
for(int i=0;i<50000;i++) {
if(i%2==0) {
System.out.println(Thread.currentThread().getName()+"打印0-50000直接的偶数:"+i);
}
}
}finally {
}
}
}
-------------------------------------------------------------------------------
线程2打印0-50000直接的偶数:49980
线程2打印0-50000直接的偶数:49982
线程2打印0-50000直接的偶数:49984
线程2打印0-50000直接的偶数:49986
线程2打印0-50000直接的偶数:49988
线程2打印0-50000直接的偶数:49990
线程2打印0-50000直接的偶数:49992
线程2打印0-50000直接的偶数:49994
线程2打印0-50000直接的偶数:49996
线程2打印0-50000直接的偶数:49998
50个子线程花了多长时间:15秒
Semaphore控同时访问的线程个数
Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
Semaphore使用例子
假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:
大致应用
class SemaphoreThread implements Runnable{
private Semaphore semaphore;
public SemaphoreThread(Semaphore semaphore) {
this.semaphore=semaphore;
}
@Override
public void run() {
try {
semaphore.acquire(); //获取许可
semaphore.release(); //释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
例:一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5); //5个许可
SemaphoreThread spt=new SemaphoreThread(semaphore);
for(int i=1;i<9;i++) {
new Thread(spt,""+i).start();
}
}
}
class SemaphoreThread implements Runnable{
private Semaphore semaphore;
public SemaphoreThread(Semaphore semaphore) {
this.semaphore=semaphore;
}
@Override
public void run() {
try {
semaphore.acquire(); //获取许可
System.out.println("工人"+Thread.currentThread().getName()+"占用一台机器在生产....");
Thread.sleep(2000); //沉睡两秒,模拟生产的时间,不然看不出效果,再说了,生产不花时间的吗?如果要这样认为,我也无语了
System.out.println("工人"+Thread.currentThread().getName()+"离开机器了。。。。");
semaphore.release(); //释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
---------------------------------------------------------------------------------------
工人2占用一台机器在生产....
工人5占用一台机器在生产....
工人4占用一台机器在生产....
工人3占用一台机器在生产....
工人1占用一台机器在生产....
工人2离开机器了。。。。
工人1离开机器了。。。。
工人5离开机器了。。。。
工人4离开机器了。。。。
工人3离开机器了。。。。
工人8占用一台机器在生产....
工人6占用一台机器在生产....
工人7占用一台机器在生产....
工人7离开机器了。。。。
工人8离开机器了。。。。
工人6离开机器了。。。。
CyclicBarrier
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程都到齐了才会继续干活。
CyclicBarrier使用例子
周末公司组织大巴去旅游,总共有三个景点,每个景点约定好游玩时间,一个景点结束后需要集中一起出发到下一个景点。例子中getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量
public class TestCyclicBarrier {
public static void main(String[] args) {
final CyclicBarrier cb = new CyclicBarrier(3); //初始化三个栅栏
MyThead mt = new MyThead(cb);
for(int i=0;i<3;i++){ //有三个玩,三个线程
new Thread(mt).start();
}
}
}
class MyThead implements Runnable{
private CyclicBarrier cb;
public MyThead(CyclicBarrier cb){
this.cb = cb;
}
@Override
public void run() {
try {
Thread.sleep((long)(Math.random()*10000));
System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点1,当前已有" +
(cb.getNumberWaiting()+1) + "个已经到达,"
+ (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
cb.await(); //第一个同步点,即第一个景点三个都到达了,才进行下一步
Thread.sleep((long)(Math.random()*10000));
System.out.println("线程" + Thread.currentThread().getName()
+ "即将到达集合地点2,当前已有" + (cb.getNumberWaiting()+1)
+ "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
cb.await(); //第二个同步点,即第二个景点三个都到达了,才进行下一步
Thread.sleep((long)(Math.random()*10000));
System.out.println("线程" + Thread.currentThread().getName()
+ "即将到达集合地点3,当前已有" + (cb.getNumberWaiting() + 1)
+ "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
cb.await(); //第三个个同步点,即第三个景点三个都到达了,才进行下一步
} catch (Exception e) {
e.printStackTrace();
}
}
}
-------------------------------------------------------------------------
线程Thread-2即将到达集合地点1,当前已有1个已经到达,正在等候
线程Thread-0即将到达集合地点1,当前已有2个已经到达,正在等候
线程Thread-1即将到达集合地点1,当前已有3个已经到达,都到齐了,继续走啊
线程Thread-0即将到达集合地点2,当前已有1个已经到达,正在等候
线程Thread-2即将到达集合地点2,当前已有2个已经到达,正在等候
线程Thread-1即将到达集合地点2,当前已有3个已经到达,都到齐了,继续走啊
线程Thread-0即将到达集合地点3,当前已有1个已经到达,正在等候
线程Thread-1即将到达集合地点3,当前已有2个已经到达,正在等候
线程Thread-2即将到达集合地点3,当前已有3个已经到达,都到齐了,继续走啊
JUC的线程交换器
Exchanger 用于线程之间数据交换,通过Exchanger.exchange(obj) 这个方法交换 ,如果第一个线程执行到exchange方法,它会等待第二个线程也执行到exchange, 当两个线程都执行到同步点时,这两个线程就可以交换数据。
public class ExchangerTest {
//定义两个信息变量,等下两个线线程分别操作一个,交换之
public static String msg1=null;
public static String msg2=null;
public void ea(Exchanger<String> exchanger) {
try {
msg1="1111111111";
System.out.println("msg1:"+msg1);
//进行交换
msg1=exchanger.exchange(msg1);
System.out.println("调用ea方法的线程交换数据完成!!!");
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
public void eb(Exchanger<String> exchanger) {
try {
msg2="22222222222222";
System.out.println("msg2:"+msg2);
//进行交换
msg2=exchanger.exchange(msg2);
System.out.println("调用ea方法的线程交换数据完成!!!");
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
//主方法,中创建两个局部内部类(匿名类创建线程对象)
public static void main(String[] args) {
Exchanger<String> exchanger =new Exchanger<>(); //最好用final修饰,不然有时会出错,参考局部内部类,用到方法中的变量一段的介绍
ExchangerTest exchangerTest=new ExchangerTest();
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
exchangerTest.ea(exchanger);
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
exchangerTest.eb(exchanger);
}
});
t1.start();
t2.start();
try {
t1.join(); //让t1执行完毕后,再执行其它
//t2.join(); //join方法必须在线程start方法调用之后调用才有意义
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("交换完毕后:msg1:"+msg1);
System.out.println("交缺钱完毕后:msg2:"+msg2);
}
}
-------------------------------------------------------------------------------
msg1:1111111111
msg2:22222222222222
调用ea方法的线程交换数据完成!!!
调用ea方法的线程交换数据完成!!!
交换完毕后:msg1:22222222222222
交缺钱完毕后:msg2:1111111111
线程八锁总结
多线程的八个案例
通过分析代码,推测打印结果,并运行代码进行验证。通过这个过程,加深对synchronized 方法的锁的理解。
1.两个线程调用同一个对象的两个同步方法
public class Demo1 {
public static void main(String[] args) {
Number number =new Number(); //锁对象是number,谁调用谁是锁
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
number.getTwo();
}
}).start();
}
}
class Number{
public synchronized void getOne() {
try {
Thread.sleep(1000); //沉睡会挂起其它线程,但有锁不会释放
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("One");
}
public synchronized void getTwo() {
System.out.println("Two");
}
}
--------------------------------------------------------------
One
Two
被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行,即便先调用的沉睡一段时间,锁也不会释放,直到程序任务完成,没有锁的方法不受锁的影响,会先执行。
2.方法1上加Three.sleep(1000)
public class Demo1 {
public static void main(String[] args) {
Number number =new Number(); //锁对象是number,谁调用谁是锁
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
number.getTwo();
}
}).start();
}
}
class Number{
public synchronized void getOne() {
try {
Thread.sleep(1000); //沉睡会挂起其它线程,但有锁不会释放
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("One");
}
public synchronized void getTwo() {
System.out.println("Two");
}
}
---------------------------------------------------------------------------
One
Two
3.添加第三个方法,它没有sychronized修饰
public class Demo1 {
public static void main(String[] args) {
Number number =new Number(); //锁对象是number,谁调用谁是锁
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
number.getTwo();
}
}).start();
}
new Thread(new Runnable() {
@Override
public void run() {
number.getThree();
}
}).start();
}
class Number{
public synchronized void getOne() {
try {
Thread.sleep(1000); //沉睡会挂起其它线程,但有锁不会释放
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("One");
}
public synchronized void getTwo() {
System.out.println("Two");
}
public void getThree(){
System.out.println("Three");
}
}
----------------------------------------------------------------------
Three
One
Two
4两个线程调用两个对象的同步方法,其中一个方法有Thread.sleep()
public class Demo2 {
public static void main(String[] args) {
Number2 number1 =new Number2(); //锁对象是number,谁调用谁是锁
Number2 number2=new Number2();
new Thread(new Runnable() {
@Override
public void run() {
number1.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
number2.getTwo();
}
}).start();
}
}
class Number2{
public synchronized void getOne() {
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("One");
}
public synchronized void getTwo() {
System.out.println("Two");
}
}
--------------------------------------------------------------
Two
One
结果说明:
被synchronized修饰的方法,锁的对象是方法的调用者。因为用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
5将有Thread.sleep()的方法设置为static方法,并且让两个线程用同一个对象调用两个方法
public class Demo2 {
public static void main(String[] args) {
Number2 number =new Number2(); //锁对象是number,谁调用谁是锁
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
number.getTwo();
}
}).start();
}
}
class Number2{
public static synchronized void getOne() {
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("One");
}
public synchronized void getTwo() {
System.out.println("Two");
}
}
------------------------------------------------------------------------
Two
One
结果说明:
被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法锁的对象不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
6将两个方法均设置为static方法,并且让两个线程用同一个对象调用两个方法
public class Demo2 {
public static void main(String[] args) {
Number2 number =new Number2(); //锁对象是number,谁调用谁是锁
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
number.getTwo();
}
}).start();
}
}
class Number2{
public static synchronized void getOne() {
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("One");
}
public static synchronized void getTwo() {
System.out.println("Two");
}
}
------------------------------------------------------------------------
One
Two
7将两个方法中有Thread.sleep()的方法设置为static方法,另一个方法去掉static修饰,让两个线程用两个对象调用两个方法
Two
One
结果说明:
被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方法,锁的对象是方法的调用者。即便是用同一个对象调用两个方法,锁的对象也不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
8将两个方法均设置为static方法,并且让两个线程用同一个对象调用两个方法
One
Two
结果说明:
被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,即便用了两个不同的对象调用方法,两个方法用的还是同一个锁,后调用的方法需要等待先调用的方法。
总结
万变不离其中,静态的同步方法,锁都是类的class对象,普通的同步方法,锁就是方法的调用者。