多线程的环境下,由于多个公共资源可能会被多个线程共享,也就是多个线程可能会操作(增、删、改等)同一资源。当多个线程操作同一资源时,很容易导致数据错乱,或发生数据安全问题(数据有可能丢失,有可能增加,有可能错乱)。
两个线程对初始值为 0 的静态变量一个做自增,一个自减,各做5000次
public class Test11 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
},"t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count--;
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count的结果:" + count);
}
}
说明:
结果可能是正数、负数、零。
原因: Java 中对静态变量的自增、自减并不是原子操作。
一个程序运行多个线程本身没有问题的
问题出在多个线程访问 共享资源
- 多个线程读 共享资源 其实也没有问题
- 在多个线程对 共享资源 读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对 共享资源 的多线程读写操作,称这段代码块为 临界区 。
static int count = 0;
// 临界区
static void increment(){
count++;
}
// 临界区
static void decrement(){
count--;
}
多个线程在临界区内执行,由于代码的 执行序列不同 而导致结果无法预测,称之为发生了 竟态条件
synchronized
是 Java 中的关键字,是一种同步锁。在多线程开发中经常会使用到这个关键字,其主要作用是可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,同时保证一个线程操作的数据变化被其他线程所看到。
synchronized
实际是用 对象锁 保证了 临界区内代码的原子性 ,临界区内的代码对外是不可分割的,不会被线程切换所打断。
在现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。如(礼品发放,每个人都想获得,天然的解决方案就是,在派发前,排队,前一个人领取完成后,后一个人再领取)。
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的 等待池形成队列 ,等待前面的线程使用完毕后,下一个线程再使用。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入 锁机制(synchronized) ,当一个线程获得对象的排它 锁 ,独占资源,其他线程必须等待,使用后释放锁即可。
存在问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起。
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
为了避免临界区的竟态条件发生,有多种手段可以达到目的。
synchronized
、Lock
注意:
虽然 Java 中互斥和同步都可以采用
synchronized
关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竟态条件发生,同一时刻只能有一个线程执行临界区代码。
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其他线程运行到某个点。
只需在方法上加
synchronized
关键字非静态的同步方法的锁对象是
this
, 静态的同步方法的锁对象是该类的字节码(类名.class
),如果锁对象是引用数据类型的成员变量,必须是静态的。缺陷:**若将一个大的方法声明为
synchronized
将会大大影响效率。 **
/**
* 线程安全,数据无负数、无相同的情况
* @author admin
*
*/
public class Test01 {
public static void main(String[] args) {
//一份资源
Web12306 t = new Web12306();
System.out.println(Thread.currentThread().getName());
//多个代理
new Thread(t,"黄牛1").start();
new Thread(t,"黄牛2").start();
new Thread(t,"黄牛3").start();
}
}
class Web12306 implements Runnable{
private int num = 10;
private boolean flag = true;
@Override
public void run() {
while(true) {
Ticket();
}
}
//加入synchronized关键字
public synchronized void Ticket() {
if (num <= 0) {
flag = false;
return;
}
//模拟网络延迟,放大发生问题的可能性
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了,还有"+ --num +"张票");
}
}
synchronized(obj){}
:obj 称之为同步监视器
- obj 可以是任何对象,但是推荐使用共享资源作为同步监视器。
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器是
this
即该对象本身,或类名.class
- 一般来说是锁的要修改的那个对象。
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码。
- 第二个线程访问,发现同步监视器被锁定,无法访问。
- 第一个线程访问完毕,解锁同步监视器。
- 第二个线程访问,发现同步监视器未上锁,就锁住并访问。
/**
* 线程安全,数据无负数、无相同的情况
* @author admin
*
*/
public class Test01 {
public static void main(String[] args) {
//一份资源
Web12306 t = new Web12306();
System.out.println(Thread.currentThread().getName());
//多个代理
new Thread(t,"黄牛1").start();
new Thread(t,"黄牛2").start();
new Thread(t,"黄牛3").start();
}
}
class Web12306 implements Runnable{
private int num = 10;
private boolean flag = true;
@Override
public void run() {
while(true) {
Ticket();
}
}
//尽可能锁定合理的范围(不是指代码,是指数据的完成性)
public void Ticket() {
//考虑没有票的情况下
if (num <= 0) {
flag = false;
return;
}
synchronized (this){
//考虑最后一张票
if (num <= 0) {
flag = false;
return;
}
//模拟网络延迟,放大发生问题的可能性
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了,还有"+ --num +"张票");
}
}
}
如果它们没有共享,则线程安全。
如果它们被共享了,根据它们的状态是否能够改变,又分为两种情况
如果只有读操作,则线程安全。
如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是线程安全的。
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用访问,它是线程安全的。
如果该对象逃离方法的作用范围,需要考虑线程安全。
public class Test01 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
test1();
},"t1");
Thread t2 = new Thread(()->{
test1();
},"t2");
t1.start();
t2.start();
}
public static void test1(){
int i = 10;
i++;
System.out.println(i);
}
}
说明: 每个线程调用 test1() 方法时局部变量 i ,会在每个线程的栈帧内存中被创建多份,因此不存在共享。
public class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber){
for (int i = 0; i < loopNumber; i++) {
//临界区,会产生竟态条件
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
调用:
public class Test01 {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe t = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(()->{
t.method1(LOOP_NUMBER);
},"thread" + i).start();
}
}
}
可能会出现这么一种可能,如果线程1还没有add,线程0的remove就会报错
Exception in thread "thread0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at com.day22.ThreadUnsafe.method3(ThreadUnsafe.java:20)
at com.day22.ThreadUnsafe.method1(ThreadUnsafe.java:11)
at com.day22.Test01.lambda$main$0(Test01.java:11)
at java.lang.Thread.run(Thread.java:748)
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与mothod2 分析相同
将 list 修改为局部变量,就不会出现上面的问题了
public class ThreadUnsafe {
public void method1(int loopNumber){
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
//临界区,会产生竟态条件
method2(list);
method3(list);
}
}
private void method2((ArrayList<String> list) {
list.add("1");
}
private void method3((ArrayList<String> list) {
list.remove(0);
}
}
分析:
- list 是局部变量,每个线程调用时会创建不同实例,没有共享。
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象。
- method3 的参数分析与 method2 相同。
- String
- Integer
- StringBuffer
- Random
- Vector
- HashTable
- java.util.concurrent 包下的类
这里线程安全是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:
Monitor 被翻译为 监视器 或 管程
1、理解java对象头
实例对象:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
对象头的主要结构是由 Mark Word 和 Class Metadata Address 组成
Mark Word 字段存储着锁的状态,轻量级锁指针,重量级锁指针。
重量级锁也就是通常说 synchronized 的对象锁,锁标识位为10,其中指针指向的 monitor 对象(也称为管程或监视器锁)的起始地址。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。但当一个 monitor 被某个线程持有后,它便处于锁定状态。
64位Mark Word结构:
说明:
- 刚开始
Monitor
中Owner
为null
。- 当 Thread-2 执行
synchronizd(obj)
就会将Monitor
的所有者 Owner 设置为 Thread-2,Monitor 中只能有一个Owner。- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行
synchronized(obj)
,就会进入 EntryList BLOCKED。- Thread-2 执行完成同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的。
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程。
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果。
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则。
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
。
假设有两个方法同步块。利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
01
:无锁。00
:轻量级锁。锁记录地址和状态 00
,表示由该线程给对象加锁。如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块
}
}
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
线程1(core 1 上) | 对象 Mark Word | 线程2(core 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步代码块,获取 monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步代码块 | 10(重量锁)重量锁指针 | - |
执行同步代码块 | 10(重量锁)重量锁指针 | 访问同步代码块,获取 monitor |
执行同步代码块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行完毕 | 10(重量锁)重量锁指针 | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁)重量锁指针 | 成功(加锁) |
- | 10(重量锁)重量锁指针 | 执行同步代码块 |
- | … | … |
自旋重试失败的情况
线程1(core 1 上) | 对象 Mark Word | 线程2(core 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步代码块,获取 monitor | 10(重量锁)重量锁指针 | - |
成功(加锁) | 10(重量锁)重量锁指针 | - |
执行同步代码块 | 10(重量锁)重量锁指针 | - |
执行同步代码块 | 10(重量锁)重量锁指针 | 访问同步代码块,获取 monitor |
执行同步代码块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步代码块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步代码块 | 10(重量锁)重量锁指针 | 自旋重试 |
执行同步代码块 | 10(重量锁)重量锁指针 | 阻塞 |
- | … | … |
说明:
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就减少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能。
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要进行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS 操作,以后只要不发生竞争,这个对象就归该线程所有。
static final Object obj = new Object();
public static void m1(){
synchronized(obj){
//同步块A
m2();
}
}
public static void m2(){
synchronized(obj){
//同步块B
m3();
}
}
public static void m3(){
synchronized(obj){
//同步块C
}
}
一个对象创建时:
-XX:BiasedLockingStartupDelay=0
来禁用延迟。测试延迟
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.10version>
dependency>
public class TestBiased {
public static void main(String[] args) {
Dog dog = new Dog();
// 无锁
System.out.println("synchronized前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
System.out.println("synchronized中");
// 偏向锁
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
// 偏向锁
System.out.println("synchronized后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}
class Dog{
}
注意:
处于偏向锁的对象解锁后,线程 id 仍存储于对象头中。
测试禁用
在运行时添加 VM 参数 -XX:-UseBiasedLocking
禁用偏向锁
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。
public class TestBiased {
public static void main(String[] args) {
Dog dog = new Dog();
//会禁用这个对象的偏向锁
dog.hashCode();
// 无锁
System.out.println("synchronized前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
System.out.println("synchronized中");
// 偏向锁
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
// 偏向锁
System.out.println("synchronized后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
}
class Dog{
}
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
public class TestBiased {
public static void main(String[] args) {
Dog dog = new Dog();
new Thread(()->{
// 无锁
System.out.println("synchronized前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
System.out.println("synchronized中");
// 偏向锁
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
// 偏向锁
System.out.println("synchronized后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (TestBiased.class){
TestBiased.class.notify();//唤醒
}
},"t1").start();
new Thread(()->{
synchronized (TestBiased.class){
try {
TestBiased.class.wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 无锁
System.out.println("synchronized前");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog){
System.out.println("synchronized中");
// 偏向锁
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
}
// 偏向锁
System.out.println("synchronized后");
System.out.println(ClassLayout.parseInstance(dog).toPrintable());
},"t2").start();
}
}
class Dog{
}
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 t1 的对象仍有机会重新偏向 t2,重偏向会重置对象的 Thread ID。
当撤销偏向锁阈值超过 20 次后,jvm会觉得,是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程。
public class TestBiased {
public static void main(String[] args) {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(()->{
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
}
synchronized (list) {
list.notify();
}
},"t1");
t1.start();
Thread t2 = new Thread(()->{
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("===================================================>");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
System.out.println(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
}
},"t2");
t2.start();
}
}
class Dog{
}
当撤销偏向锁阈值超过 40 次后,jvm 会觉得,确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
public class TestBiased {
static Thread t1,t2,t3;
public static void main(String[] args) throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(()->{
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
}
LockSupport.unpark(t2);
},"t1");
t1.start();
t2 = new Thread(()->{
LockSupport.park();
System.out.println("=============================================>");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
System.out.println(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
}
LockSupport.unpark(t3);
},"t2");
t2.start();
t3 = new Thread(()->{
LockSupport.park();
System.out.println("=============================================>");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
System.out.println(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
}
},"t3");
t3.start();
t3.join();
//发现不会再偏向了
System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());
}
}
class Dog{
}
1、原理:
说明:
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态。
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占 CPU 时间片。
- BLOCKER 线程会在 Owner 线程释放锁时唤醒。
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后不意味着立刻获得锁,仍需进入 EntryList 重新竞争。
2、API介绍
obj.wait()
:让进入 object 监视器的线程到 waitSet 等待。
obj.notify()
:在 object 上正在 waitSet 等待的线程中挑一个唤醒。
obj.notifyAll()
:让 object 上正在 waitSet 等待的线程全部唤醒。
总结: 它们都是线程间进行协作的手段,都属于 object 对象的方法,**必须获得此对象的锁,**才能调用这几个方法
public class TestBenchmark {
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (obj) {
System.out.println("执行...");
try {
obj.wait();//让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("其他代码1...");
}
}).start();
new Thread(()->{
synchronized (obj) {
System.out.println("执行...");
try {
obj.wait();//让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("其他代码2....");
}
}).start();
//主线程两秒后执行
sleep(2000);
System.out.println("唤醒 obj 上其他线程");
synchronized (obj) {
obj.notify();//唤醒obj上一个线程
// obj.notifyAll();//唤醒obj上所有等待线程
}
}
}
wait()
方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就有机会获取对象的锁。无限制等待,直到notify 为止。
wait(long n)
有时限的等待,等待 n 毫秒后结束等待,或者被 notify。
3、sleep 和 wait 的区别
4、wait 和 notifyAll 正确使用格式
synchronized(lock) {
while(条件不成立){
lock.wait();
}
//干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
1、定义
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待。
2、实现
class GuardedObject{
//结果
private Object response;
//获取结果
public Object get(){
synchronized (this) {
//条件不满足就等待
while (response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}
//产生结果
public void complete(Object response) {
synchronized (this) {
//条件满足,唤醒等待的线程
this.response = response;
this.notifyAll();
}
}
}
3、应用
一个线程等待另一个线程的结果
public class Test03 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
//等待结束
System.out.println("开始...");
List<String> list = (List<String>) guardedObject.get();
System.out.println("结果大小:" + list.size());
},"t1").start();
new Thread(()->{
System.out.println("执行...");
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
//模拟延时等待3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guardedObject.complete(list);
},"t2").start();
}
}
执行结果:
开始...
执行...
结果大小:3
4、带超时版GuardedObject
public class Test03 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(()->{
//等待结束
System.out.println("开始...");
Object obj = guardedObject.get(2000);
System.out.println("结果:" + obj);
},"t1").start();
new Thread(()->{
System.out.println("执行...");
//等待3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
guardedObject.complete(list);
},"t2").start();
}
}
class GuardedObject{
//结果
private Object response;
//获取结果
public Object get(long timeout){
synchronized (this) {
//记录开始时间
long begin = System.currentTimeMillis();
//经历时间
long passedTime = 0;
//条件不满足就等待
while (response == null) {
//这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
//经历时间超过最大等待时间时,退出循环
if (waitTime <=0 ) {
break;
}
try {
this.wait(waitTime);//避免虚假唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
//计算经历时间
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
//产生结果
public void complete(Object response) {
synchronized (this) {
//条件满足,唤醒等待的线程
this.response = response;
this.notifyAll();
}
}
}
执行结果:
开始...
执行...
结果:null
5、join原理
是调用者轮询检查线程 alive 状态,join 体现的是保护性暂停模式
t1.join();
等价于下面代码
synchronized(t1) {
//调用者线程进入 t1 的 waitSet 等待,直到 t1 运行结束
while(t1.isAlive()){
t1.wait();
}
}
join底层源码
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();//记录当前时间
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
//本次循环需要等待的时间
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
//计算经历时间
now = System.currentTimeMillis() - base;
}
}
}
1、定义要点
2、实现与应用
public class Test04 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
//创建生产者
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(()->{
queue.put(new Message(id,"生产消息" + id));
},"生产者" + i).start();
}
new Thread(()->{
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//消费消息
Message take = queue.take();
}
},"消费者").start();
}
}
class MessageQueue{
//消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
//队列容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
//获取消息
public Message take(){
synchronized (list) {
//检查队列是否为空
while (list.isEmpty()) {
try {
System.out.println("【" + Thread.currentThread().getName() + "】 - 队列为空,消费者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//从队列头部获取消息并返回
Message message = list.removeFirst();
System.out.println("【" + Thread.currentThread().getName() + "】 - 已消费消息:" + message);
list.notifyAll();//唤醒等待,每消费一个唤醒生产
return message;
}
}
//存入消息
public void put(Message message) {
synchronized (list) {
//检查队列是否已满
while (list.size() == capcity) {
try {
System.out.println("【" + Thread.currentThread().getName() + "】 - 队列已满,生产者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//将新消息加入队列尾部
list.addLast(message);
System.out.println("【" + Thread.currentThread().getName() + "】 - 已生产消息:" + message);
list.notifyAll();
}
}
}
final class Message{
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
执行结果:
【生产者0】 - 已生产消息:Message{id=0, value=生产消息0}
【生产者2】 - 已生产消息:Message{id=2, value=生产消息2}
【生产者1】 - 队列已满,生产者线程等待
【消费者】 - 已消费消息:Message{id=0, value=生产消息0}
【生产者1】 - 已生产消息:Message{id=1, value=生产消息1}
【消费者】 - 已消费消息:Message{id=2, value=生产消息2}
【消费者】 - 已消费消息:Message{id=1, value=生产消息1}
【消费者】 - 队列为空,消费者线程等待
1、基本使用
它们是 LockSupport
类中的方法
//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
2、实例
public class Test05 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("【" + Thread.currentThread().getName() + "】 - start...");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【" + Thread.currentThread().getName() + "】 - park...");
//上锁
LockSupport.park();
System.out.println("【" + Thread.currentThread().getName() + "】 - end...");
},"t1");
t1.start();
sleep(2000);
System.out.println("【" + Thread.currentThread().getName() + "】 - unpark...");
//解锁
LockSupport.unpark(t1);
}
}
执行效果:
【t1】 - start...
【t1】 - park...
【main】 - unpark...
【t1】 - end...
3、特点
与 Object 的 wait & notify 相比:
4、原理
每一个线程都有自己的一个 Parker 对象,由三部分组成 _counter
, _cond
和 _mutex
。
举例说明:
- 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0耗尽,1充足)。
- 调用 park 就是要看需不需要停下来歇息
- 如果干粮耗尽,那么钻进帐篷休息。
- 如果干粮充足,那么不需要停留,继续前进。
- 调用 unpark ,就好比干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进。
- 如果这时线程还在运行,那么下次他调用 park 时,仅仅消耗干粮,不需要停留,继续前进。因为背包空间有限,每次调用 unpark 仅会补充一份干粮。
1.当前线程调用 Unsafe.park() 方法。
2.检查 _counter,若为0,这时,获得 _mutex 互斥锁。
3.线程进入 _cond 条件变量阻塞。
4.设置 _counter = 0。
1.调用 Unsafe.unpark(Thread_0) 方法,设置 _counter = 1。
2.唤醒 _cond 条件变量中的 Thread_0。
3.Thread_0 恢复运行。
4.设置 _counter = 0。
1.调用 Unsafe.unpark(Thread_0) 方法,设置 _counter = 1。
2.当前线程调用 Unsafe.park() 方法。
3.检查 _counter,若为1,这时线程无需阻塞,继续运行。
4.设置 _counter = 0。
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被创建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统称为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或者中断) |
TIMED_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
假设有线程 Thread t
1、NEW --> RUNNABLE
状态
t.start()
方法时,由 NEW --> RUNNABLE
2、RUNNABLE <--> WAITING
状态
t 线程 用 synchronized(obj)
获取了对象锁后
obj.wait()
方法时,t 线程从 RUNNABLE --> WAITING
obj.notify()
, obj.notifyAll()
, 线程对象.interrupt()
时
WAITING --> RUNNABLE
WAITING --> BLOCKED
3、RUNNABLE <--> WAITING
状态
t.join()
方法,当前线程从 RUNNABLE --> WAITING
interropt()
时,当前线程从 WAITING --> RUNNABLE
4、RUNNABLE <--> WAITING
状态
LockSupport.park()
方法会让当前线程从 RUNNABLE --> WAITING
LockSupport.unpark(目标线程)
或调用了线程的 interrupt()
,会让目标线程从 WAITING --> RUNNABLE
5、RUNNABLE <--> TIMED_WAITING
状态
t 线程 用 synchronized(obj)
获取了对象锁后
obj.wait(long n)
方法时,t 线程 从 RUNNABLE --> TIMED_WAITING
obj.notify()
, obj.notifyAll()
, t.interrupt()
时
TIMED_WAITING --> RUNNABLE
TIMED_WAITING --> BLOCKED
6、RUNNABLE <--> TIMED_WAITING
状态
t.join(long n)
方法时,当前线程从 RUNNABLE --> TIMED_WAITING
interrupt()
时,当前线程从 TIMED_WAITING --> RUNNABLE
7、RUNNABLE <--> TIMED_WAITING
状态
Thread.sleep(long n)
,当前线程从 RUNNABLE --> TIMED_WAITING
TIMED_WAITING --> RUNNABLE
8、RUNNABLE <--> TIMED_WAITING
状态
LockSupport.parkNanos(long nanos)
或 LockSupport.parkUntil(long millis)
时,当前线程从 RUNNABLE --> TIMED_WAITING
LockSupport.unpark(目标线程)
或调用了线程的 interrupt()
,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE
9、RUNNABLE <--> BLOCKED
状态
synchronized(obj)
获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
BLOCKED
的线程重新竞争,如果其中 t 线程 竞争 成功,从 BLOCKED --> RUNNABLE
,其它失败的线程仍然 BLOCKED
10、RUNNABLE <--> TERMINATED
状态
TERMINATED
多把不相干的锁
public class Test06 {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(()->{
try {
bigRoom.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小红").start();
new Thread(()->{
try {
bigRoom.study();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小兰").start();
}
}
class BigRoom{
private final Object studyRoom = new Object();
private final Object sleepRoom = new Object();
public void sleep() throws InterruptedException {
synchronized (sleepRoom) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 休息2小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (studyRoom) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 学习1小时");
Thread.sleep(1000);
}
}
}
执行结果:
Wed Jul 08 17:02:32 CST 2020【小兰】 - 学习1小时
Wed Jul 08 17:02:32 CST 2020【小红】 - 休息2小时
优点和缺点:
优点
:可以增强并发度。
缺点
:如果一个线程需要同时获得多把锁,就容易造成死锁现象。
1、死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。相互不释放资源从而相互等待
public class Test07 {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(()->{
synchronized (A) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
}
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (B) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
try {
sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
}
}
},"t2");
t1.start();
t2.start();
}
}
执行结果:
Wed Jul 08 17:31:06 CST 2020【t2】 - Lock B
Wed Jul 08 17:31:06 CST 2020【t1】 - Lock A
解决死锁:
public class Test07 {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(()->{
synchronized (A) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (B) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (B) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
try {
sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (A) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
}
},"t2");
t1.start();
t2.start();
}
}
总结:不要在同一个代码块中,同时持有多个对象的锁就能避免死锁发生。
使用顺序加锁的方式解决死锁问题:
线程1 已经获得 对象A 的锁,线程2 获得 对象B 的锁,线程1 尝试获取 对象B 的锁,线程2 尝试获取 对象A 的锁,这样就导致死锁的发生。
顺序加锁的解决方法:
线程1 获取 对象A 的锁,线程2 再想获取 对象A 的锁已经获取不到,就会进入 对象A 的 entryList 里阻塞,线程1 再尝试获取 对象B 的锁。
2、定位死锁
- 使用 jps 定位进程 id。
- 再用 jstack 定位死锁。
3、活锁
活锁出现在两个线程互相改变对方的结束条件,最后造成都无法结束的情况。解决方法:交错执行或设置随机睡眠时间。
public class Test08 {
static volatile int count = 10;
static final Object obj = new Object();
public static void main(String[] args) {
new Thread(()->{
while (count > 0) {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("count:" + count);
}
},"t1").start();
new Thread(()->{
while (count < 20) {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println("count:" + count);
}
},"t2").start();
}
}
4、饥饿
在java中,下面三个常见的原因会导致线程饥饿:
1、特点
相对于 synchronized 它具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁(防止线程饥饿)
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
2、基本语法
//获取锁,获取不到就会等待
reentrantLock.lock();//放try里面外面都是一样的
try {
//临界区
} finally {
//释放锁
reentrantLock.unLock();
}
3、可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
public class Test01 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
//上锁
reentrantLock.lock();
try {
System.out.println("main");
m1();
} finally {
//释放锁
reentrantLock.unlock();
}
}
public static void m1(){
//上锁
reentrantLock.lock();
try {
System.out.println("m1");
m2();
} finally {
//释放锁
reentrantLock.unlock();
}
}
public static void m2(){
//上锁
reentrantLock.lock();
try {
System.out.println("m2");
} finally {
//释放锁
reentrantLock.unlock();
}
}
}
4、可打断
lockInterruptibly()
方法返回的为获取锁的方法,但是当线程调用了 interrupt()
方法后,此方法将会返回一个异常,导致线程的中断。即线程中断。
public class Test02 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
//如果没有竞争那么此方法就会获取 reentrantLock 对象锁
//如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法打断
System.out.println("尝试获得锁");
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("没有获得锁,返回");
return;
}
try {
System.out.println("获取到锁");
} finally {
reentrantLock.unlock();
}
}, "t1");
reentrantLock.lock();
t1.start();
sleep(1000);
System.out.println("打断t1");
t1.interrupt();
}
}
执行结果:
注意: 如果是不可中断模式,那么即使使用了 interrupt
也不会让等待中断
public class Test02 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("尝试获得锁");
reentrantLock.lock();
try {
System.out.println("获取到锁");
} finally {
reentrantLock.unlock();
}
}, "t1");
reentrantLock.lock();
System.out.println("获得到锁");
t1.start();
sleep(1000);
System.out.println("打断t1");
t1.interrupt();
}
}
执行结果:
5、锁超时
tryLock()
方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true
,如果获取失败(即锁已被其他线程获取),则返回 false
,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
获取不到锁,立刻结束等待:
public class Test03 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 尝试获得锁");
if (!reentrantLock.tryLock()) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取不到锁");
return;
}
try {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取到锁");
} finally {
reentrantLock.unlock();
}
}, "t1");
reentrantLock.lock();
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 主线程先于对象上锁");
t1.start();
sleep(1000);
reentrantLock.unlock();
}
}
获取不到锁,等待时间结束等待:
public class Test03 {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 尝试获得锁");
try {
if (!reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取不到锁,等待 1s 结束");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取到锁");
} finally {
reentrantLock.unlock();
}
}, "t1");
reentrantLock.lock();
System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 主线程先于对象上锁");
t1.start();
sleep(2000);
reentrantLock.unlock();
}
}
执行结果:
Thu Jul 09 14:22:00 CST 2020【main】 - 主线程先于对象上锁
Thu Jul 09 14:22:00 CST 2020【t1】 - 尝试获得锁
Thu Jul 09 14:22:01 CST 2020【t1】 - 获取不到锁,等待 1s 结束
6、公平锁
ReentrantLock 默认是不公平的。公平锁一般没有必要,会降低并发度。
7、条件变量
synchronized
中也有条件变量,当条件不满足时进入 waitSet
等待。
ReentrantLock
的条件变量比 synchronized
强大之处在于,它是支持多个条件变量的
使用要点:
public class Test04 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
//等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
//等待外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
ROOM.lock();
try {
System.out.println("小红:有烟没?" + hasCigarette);
while (!hasCigarette){
System.out.println("小红:没烟,先歇会!");
try {
waitCigaretteSet.await();//进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("小红:可以干活了!");
} finally {
ROOM.unlock();
}
},"小红").start();
new Thread(()->{
ROOM.lock();
try {
System.out.println("小兰:外卖送到了吗?" + hasTakeout);
while (!hasTakeout){
System.out.println("小兰:没外卖,先歇会!");
try {
waitTakeoutSet.await();//进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("小兰:可以干活了");
} finally {
ROOM.unlock();
}
},"小兰").start();
sleep(1000);
new Thread(()->{
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();//唤醒等待
} finally {
ROOM.unlock();
}
},"送外卖的").start();
sleep(1000);
new Thread(()->{
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();//唤醒等待
} finally {
ROOM.unlock();
}
},"送烟的").start();
}
}