管程

1、同步机制保证线程安全的方式

  1. synchronized同步代码块
  2. synchronized同步方法
  3. Lock锁

不管哪种方法都必须保证锁必须是唯一的

有线程共享同一数据,就要注意线程的安全性

1、什么是线程安全问题?

线程安全问题中的共享变量是哪里的?

  1. 多个线程共享统一资源,当多个线程同时操作该资源就会出现线程安全问题
  2. 采用同步机制保证线程安全

package atguigu.java;

/**
* 例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
* 存在线程的安全问题,待解决。
*
* @author shkstart
* @create 2019-02-13 下午 4:47
*/
class Window1 implements Runnable{

private int ticket = 100;

@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}


public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}

}

2、synchronized

  1. java中每个对象都可以作为锁,这是synchronized实现同步的基础
  2. 普通同步方法:锁是当前实例对象
  3. 静态同步方法:锁是当前类的class对象
  4. 同步方法块:琐是括号里的对象

jvm在我们的代码前后加上了monitor和monitorexit,通过这个实现锁的功能,、反编译结果里面有两个monitorexit,这是jvm为了保证成功释放监视器,做的一个兜底操作

为什么是重量级锁?

synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。 如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。

用synchronized编写代码,在多核CPU上执行所需的成本也比你想象的要高。

多核CPU的每个处理器内核都有独立的高速缓存。加锁需要这些高速缓存同步运行,然而这又需要在内核间进行较慢的缓存一致性协议通信

2.1 理解

  1. synchronized表示同步,可以解决多个线程之间访问资源的同步问题,能够保证被它修饰的方法或者代码在任意时刻只能有一个线程执行
  2. 用于为Java对象、方法、代码块提供线程安全的操作,属于排它的悲观锁也属于可重入锁
  3. 被synchronized修饰的方法和代码块在同一时刻只能有一个线程访问,其他线程只有等待当前线程释放锁资源后才能访问。
  4. Java中的每个对象都有一个monitor监视器对象,加锁就是在竞争monitor,对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方是否加锁是通过一个标记位来判断的

2.2 synchronized的内部都包括哪些区域?

synchronized内部包括6个不同的区域,每个区域的数据都代表锁的不同状态。

  1. ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中。
  2. EntryList:竞争候选列表,在锁竞争队列中有资格成为候选者来竞争锁资源的线程被移动到候选列表中。
  3. WaitSet:等待集合,调用wait方法后阻塞的线程将被放在WaitSet。
  4. OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck。
  5. Owner:竞争到锁资源的线程状态。
  6. !Owner:释放锁后的状态。

2.3 synchronized的原理

  1. 收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,被放入ContentionList(该做法对于已经进入队列的线程是不公平的,体现了synchronized的不公平性)。
  2. 为了防止ContentionList尾部的元素被大量线程进行CAS访问影响性能,Owner线程会在是释放锁时将ContentionList的部分线程移动到EntryList并指定某个线程(一般是最先进入的)为OnDeck线程。Owner并没有将锁直接传递给OnDeck线程而是把锁竞争的权利交给他,该行为叫做竞争切换,牺牲了公平性但提高了性能。
  3. 获取到锁的OnDeck线程会变为Owner线程,未获取到的仍停留在EntryList中。
  4. Owner线程在被wait阻塞后会进入WaitSet,直到某个时刻被唤醒再次进入EntryList。
  5. ContentionList、EntryList、WaitSet中的线程均为阻塞状态。
  6. 当Owner线程执行完毕后会释放锁资源并变为!Owner状态。

2.4 JVM对synchronized优化以及原理

  1. 在Java的早期版本中,synchronized属于重量级锁,效率很低。因为监视器锁依赖底层操作系统的Mutex Lock实现,Java的线程映射到操作系统的原生线程上。所以挂起和唤醒一个线程都需要操作系统帮忙完成,时间成本很高。在Java6以后从JVM层面对synchronized有了较大的优化。
  2. JDK1.6的优化:
  3. JDK1.6中引入了适应自旋、锁消除、锁粗化、轻量级锁以及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,这种过程叫做锁膨胀。
  4. JDK1.6中默认开启了偏向锁和轻量级锁,可以通过-XX:UseBiasedLocking禁用偏向锁。
  5. synchronized锁升级原理
  6. 锁对象的对象头里有一个threadid字段,在第一次访问时threadid为空,jvm让其持有偏向锁,并将threadid设置为线程id
  7. 再次进入时会判断threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致则升级偏向锁为轻量级锁,通过自旋循环一定次数获取锁
  8. 执行一定次数后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级到重量级锁,此过程就构成了synchronized锁升级

2.5 synchronized的可重入原理

  1. 重入锁:指一个线程获取到该锁后,可以继续获得该锁
  2. 底层原理:维护一个计数器,当线程获取该锁,计数器加一,再次获得该锁继续加一,释放锁时计数器减一,当计数器为0,表明该锁未被任何线程所有,其他线程可以竞争获取该锁
  3. 自己可以再次获取自己的内部锁,比如一个线程获得了某个对象的锁,此时对象锁还没有释放,当其再次想获取这个对象的锁时,还是可以获取的,如果不可锁重入的话,会造成死锁(比如:run方法中有不同的同步的代码块使用同一个对象锁,执行run方法,执行过程中,就需要再一次获取锁,因此必须可重入)。

修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例

对象,是类成员(

果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的

静态因为访问静态

是当前类的锁,而访问非静态synchronized 方法占用的锁是当前实例对象锁。

修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上

锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因

为JVM中,字符串常量池具有缓存功能

3、同步代码块方式

  1. 该方式允许同一个类中存在多个锁对象
  2. 如果想让多个线程即使访问多个不同的代码块,也要统一排队的话,可以让多个代码块(就是指run()中有多个synchronzied{ } 代码块)使用同一个锁对象
  3. 如果想让多个线程访问不同的代码块互不影响,但是访问同一个代码块需要排队等待的话,可以让多个代码块使用不同的锁对象

package com.atguigu.java;

/**
* 例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
*
* 1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
* 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
* 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他
* 线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
*
*
* 4.在Java中,我们通过同步机制,来解决线程的安全问题。
*
* 方式一:同步代码块
*
* synchronized(同步监视器){
* //需要被同步的代码
*
* }
* 说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
* 2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
* 3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
* 要求:多个线程必须要共用同一把锁。
*
* 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
* 方式二:同步方法。
* 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
*
*
* 5.同步的方式,解决了线程的安全问题。---好处
* 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 ---局限性
*
*/
class Window1 implements Runnable{

private int ticket = 100;
// Object obj = new Object();
// Dog dog = new Dog();
@Override
public void run() {
// Object obj = new Object();
while(true){
synchronized (this){//此时的this:唯一的Window1的对象 //方式二:synchronized (dog) {

if (ticket > 0) {

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);


ticket--;
} else {
break;
}
}
}
}
}


public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}

}

class Dog{

}

//方式二:继承Thread时;不考虑this,要考虑类作为锁(同步监视器)
class Window1 extends Thread{


private static int ticket = 100;

private static Object obj = new Object();

@Override
public void run() {

while(true){
//正确的
// synchronized (obj){
synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
//错误的方式:this代表着t1,t2,t3三个对象
// synchronized (this){

if(ticket > 0){

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}

}

}
}


package com.atguigu.exer;

/**
* 银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

分析:
1.是否是多线程问题? 是,两个储户线程
2.是否有共享数据? 有,账户(或账户余额)
3.是否有线程安全问题?有
4.需要考虑如何解决线程安全问题?同步机制:有三种方式。

* @author shkstart
* @create 2019-02-15 下午 3:54
*/
class Account{
private double balance;

public Account(double balance) {
this.balance = balance;
}

//存钱
public synchronized void deposit(double amt){
if(amt > 0){
balance += amt;

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
}
}
}

class Customer extends Thread{

private Account acct;

public Customer(Account acct) {
this.acct = acct;
}

@Override
public void run() {

for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}

}
}


public class AccountTest {

public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);

c1.setName("甲");
c2.setName("乙");

c1.start();
c2.start();
}
}

4、同步方法

  1. 静态
  2. 非静态
  3. https://www.cnblogs.com/studyjobs/p/15774167.html

//同步方法的锁对象是其所在类的实例化对象本身 this
修饰符
方法体
}

//同步静态方法的锁对象是其所在的类的 类名.Class
修饰符
方法体
}

/**
* 使用同步方法处理继承Thread类的方式中的线程安全问题
*
* @author shkstart
* @create 2019-02-15 上午 11:43
*/
class Window4 extends Thread {

private static int ticket = 100;

@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){//同步监视器:Window4.class
//private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}

public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();

}
}

/**
* 使用同步方法解决实现Runnable接口的线程安全问题
*
* 关于同步方法的总结:
* 1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
* 2. 非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
*/

class Window3 implements Runnable {

private int ticket = 100;

@Override
public void run() {
while (true) {

show();
}
}

private synchronized void show(){//同步监视器:this
//synchronized (this){

if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
//}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

wait和notify

wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段 标准的使用 wait 和 notify 方法的代码:

// The standard idiom for using the wait method
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}

因为执行wait()的线程,如果重新被唤醒,是从wait()代码之后继续执行的,而不是重新从该方法的头部重新执行。

假如有两个线程,向一个数组中插入数据,插入之前先判断数组大小,如果大于等于数组长度,那么wait(),否则会向数组中插入一条数据。还有其他线程是取数据的。

插入数据方法伪代码如下

synchronized void push(int number) {

if (数组满了) {

this.wait();

}

array[++index] = xxx;

}

插入线程1插入直到数组满了,执行wait()方法,然后释放锁,插入线程2判断也满了,然后也执行wait()方法,然后释放锁,这时候假如唤醒的是线程1,那么他就会执行array[++index]==xxx;会数组越界

如果改为

while (数组满了) {this.wait();}

就会重新判断一次,如果满了重新wait(),就不会发生异常了

notify可能导致死锁而notifyAll不会;通知(notify)操作可能导致死锁的原因有几种。

死锁是指两个或多个进程(线程)在互相等待对方释放资源时陷入无限等待的状态。

首先,如果在多线程环境中使用了错误的同步机制,就可能导致死锁。例如,当一个线程在持有某个锁的同时试图获取另一个锁时,而另一个线程同时持有该第二个锁并试图获取第一个锁时,就可能发生死锁。

其次,如果在使用notify操作时没有正确地释放锁,也可能导致死锁。在Java中,当一个线程调用对象的notify方法时,它必须先获得该对象的监视器锁。如果在调用notify后没有正确释放锁,其他等待该锁的线程将无法继续执行,可能导致死锁。

此外,如果多个线程在等待同一个对象的notify操作,但只有一个线程被唤醒,而其他线程仍然处于等待状态,也可能导致死锁。这种情况下,需要确保所有等待线程都能得到通知,否则可能出现死锁。

为了避免notify导致死锁,需要正确地使用同步机制和锁,并确保在调用notify后正确地释放锁。此外,应该仔细设计多线程的逻辑,避免出现多个线程等待同一个notify操作的情况。

5、Lock锁

  1. JDk5新增
  2. 实现:

package com.atguigu.java1;

import java.util.concurrent.locks.ReentrantLock;

/**
* 解决线程安全问题的方式三:Lock锁 --- JDK5.0新增
*
* 1. 面试题:synchronized 与 Lock的异同?
* 相同:二者都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
*
* 2.优先使用顺序:
à 同步代码块(已经进入了方法体,分配了相应资源) à 同步方法(在方法体之外)
*/
class Window implements Runnable{

private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while(true){
try{

//2.调用锁定方法lock()
lock.lock();

if(ticket > 0){

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}

}
}
}

public class LockTest {
public static void main(String[] args) {
Window w = new Window();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

6、总结:

解决线程安全问题的方法:

  1. 使用同步代码块时:
  2. 实现Runnable接口的方式可以使用this作为锁
  3. 但是继承Thread的方式不可以使用this作为锁,可以使用类的实例
  4. 使用同步方法时:
  5. 实现Runnable接口的方式 方法可以是非静态的,默认this作为锁;
  6. 但是继承Thread的方法必须声明为静态,默认是当前类本身
  7. Lock锁
  8. Lock锁需要手动同步和手动结束同步
  9. 与synchronized的异同?
  10. 都是解决线程安全问题
  11. synchronized机制在执行完同步代码后,会自动释放同步监视器(锁)
  12. Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
  13. 面试题:如何解决线程安全问题?有几种方式

线程安全问题和解决方式​_代码块


8、死锁

(1)造成死锁的原因

  1. 不同的线程分别占用对方的要使用的资源不放弃,都在等待对方释放自己需要的资源,就造成了线程死锁
  2. 死锁的必要条件:
  3. 互斥:一段时间内某个资源只能被某一个进程或线程占用,其他请求该资源的进程/线程只能等待
  4. 不可剥夺条件:即资源不可以被其他进程/线程强行夺走,只能自己释放
  5. 请求与保持:当前进程保持了至少一个资源,又去请求其他资源,但被其他进程占用,此时该进程被阻塞,但对自己保持的资源保持不放
  6. 循环等待:
  7. 线程同步时很容易造成死锁;所以线程同步时一定要注意避免死锁
  8. 死锁出现后,不会报异常,不会出现提示,所有线程都是阻塞状态,无法继续

(2)解决方式

https://blog.csdn.net/wljliujuan/article/details/79614019

出现死锁后不会出现异常和提示也不会终止程序,一直处于阻塞状态

如何避免死锁:

  1. 专门的算法、原则
  2. 尽量减少同步方法的定义
  3. 尽量避免嵌套同步

停止线程的方式

正常结束

使用退出标志退出线程

一般

行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接

的方法就是设一个

出,代码示例:

定义了一个退出标志

时,使用了一个

个线程来修改

public class ThreadSafe extends Thread{
public volatile boolean exit = false;
public void run(){
while(!exit){

}
}
}

Interrupt 方法结束线程

使用

  1. 线程处于阻塞状态:
  2. 如使用了
  3. 当调用线程的
  4. 线程未处于阻塞状态:
  5. 使用
  6. 当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

public class ThreadSafe extends Thread{
public void run(){
while(!isInterrupted()){ // 非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000); // 阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break; // 捕获到异常后,执行break跳出循环
}
}
}
}

stop()

程序中可以直接使用

电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之

后,创建子线程的线程就会抛出

任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有

的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏

的数据时,有可能导致一些很奇怪的应用程序错误。因

此,并不推荐使用

线程的中断

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

  1. 调用。也就是说处于
  2. 若调用
  3. 许多声明抛出都会清除中断标识位,所以抛出异常后,调用
  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程

https://developer.aliyun.com/article/1114182

CAS和Synchronized

简单的来说

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
  3. 补充: Java 并发编程这个领域中 synchronized 关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在 JavaSE 1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized 的底层实现主要依靠 Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和