临界资源问题
临界资源
在一个进程中,多个线程之间是可以资源共享的。如果在一个进程中的一个资源同时被多个线程访问,这个资源就是一个临界资源。如果多个线程同时访问临界资源,会对这个资源的值造成影响。
临界资源问题
多个线程同时访问一个资源的情况下,一个线程在操作这个资源的时候,将值取出进行运算,在还没来得及进行修改这块空间的值之前,值又被其他的线程取走了。此时就会出现临界资源的问题,造成这个资源的值出现不是我们预期的值。
解决方案
临界资源问题出现的原因就是多个线程在同时访问一个资源,因此解决方案也很简单, 就是不让多个线程同时访问即可。
在一个线程操作一个资源的时候,对这个资源进行“上锁”,被锁住的资源,其他的线程无法访问。
线程锁
线程锁,就是用来“锁住”一个临界资源,其他的线程无法访问。在程序中,可以分为对象锁和类锁
- 对象锁:任何的对象,都可以被当做是一把锁来使用。但是需要注意,必须要保证不同的线程看到的锁,需要是同一把锁才能生效。如果不同的线程看到的锁对象是不一样的,此时这把锁将没有任何意义。
- 类锁:可以将一个类做成锁,使用类.class 来作为锁。
同步代码段
同步代码段,是来解决临界资源问题最常见的方式。将一段代码放入到同步代码段中,将这段代码上锁。
第一个线程抢到了锁标记后,可以对这个临界资源上锁,操作这个临界资源。此时其他的线程再执行到synchronized的时候,会进入到锁池,直到持有锁的线程使用结束后,对这个资源进行解锁。此时,处于锁池中的线程都可以抢这个锁标记,哪一个线程抢到了,就进入到就绪态,没有抢到锁的线程,依然处于锁池中。
public class Test {
private static int ticket=100;
public static void main(String[] args) {
Runnable runnable =new Runnable() {
@Override
public void run() {
while (ticket > 0) {
/*
* 同步代码段,这里的逻辑执行,会被上锁。当这里的逻辑执行结束之后,会自动的解锁。
* 小括号中需要写的是:锁。
* 这里的锁,可以分为:类锁 和 对象锁
*/
synchronized (Thread.class){
if(ticket<=0){
break;
}
System.out.println(String.format("售票员%s卖出一张票,剩余:%d",Thread.currentThread().getName(),--ticket));
}
}
}
};
Thread t1=new Thread(runnable,"周杰伦");
Thread t2=new Thread(runnable,"林俊杰");
Thread t3=new Thread(runnable,"张杰");
Thread t4=new Thread(runnable,"薛之谦");
Thread t5=new Thread(runnable,"陈奕迅");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
同步方法
如果在一个方法中,所有的逻辑,都需要放到同一个同步代码段中执行。 这样的方法,可以直接做成同步方法。
同步方法中的所有的逻辑,都是在一个同步代码段中执行的。
如果是一个静态方法,使用当前类做类锁;如果是一个非静态方法,使用this做对象锁。
public static synchronized Chairman getInstance() {
if (Instance == null) {
Instance = new Chairman();
}
return Instance;
}
单例设计模式
饿汉式和懒汉试
public class Test {
public static void main(String[] args) {
Boss1 boss1=Boss1.getInstance();
Boss2 boss2=Boss2.getInstance();
}
}
class Boss1{
//私有化构造方法,杜绝从外界通过new的⽅式实例化对象的可能性。
private Boss1(){
System.out.println("懒汉式");
}
//声明一个私有的静态的Boss1对象
private static Boss1 instance=null;
//提供一个公有的,静态的,能够获取当前类对象的方法
public static Boss1 getInstance(){
if(instance==null){
instance=new Boss1();
}
return instance;
}
}
class Boss2{
//私有化构造方法,杜绝从外界通过new的⽅式实例化对象的可能性。
private Boss2(){
System.out.println("饿汉式");
}
//实例化一个私有的静态的Boss2对象
private static Boss2 instance=new Boss2();
//提供一个公有的,静态的,能够获取当前类对象的方法
public static Boss2 getInstance(){
return instance;
}
}
懒汉式单例,在多线程的环境下,会出现问题。由于临界资源问题的存在,单例对象可能会被实例化多次。
因此,单例设计模式,尤其是懒汉式单例,需要针对多线程的环境进行处理。
public class Boss {
private Boss() {}
private static Boss instance ;
public static synchronized Boss getInstance() {
if (instance == null) {
instance = new Boss();
}
return instance;
}
}
死锁
多个线程,同时持有对方需要的锁标记,等待对方释放自己需要的锁标记。
此时就是出现死锁,线程之间彼此持有对方需要的锁标记,而不进行释放,都在等待。
public class Test {
public static void main(String[] args) {
Runnable runnableA=()->{
synchronized ("a"){
System.out.println("线程A获取了a锁,等待获取b锁");
synchronized("b"){
System.out.println("线程A同时获取了a锁和b锁");
}
}
};
Runnable runnableB=()->{
synchronized ("b"){
System.out.println("线程B获取了b锁,等待获取a锁");
synchronized("a"){
System.out.println("线程B同时获取了b锁和a锁");
}
}
};
new Thread(runnableA,"A").start();
new Thread(runnableB,"B").start();
}
}
wait、notify
Object类中几个方法如下:
- wait()
- 等待,让当前的线程,释放自己持有的指定的锁标记,进入到等待队列。
- 等待队列中的线程,不参与CPU时间⽚的争抢,也不参与锁标记的争抢。
- notify()
- 通知、唤醒。唤醒等待队列中,⼀个等待这个锁标记的随机的线程。
- 被唤醒的线程,进⼊到锁池,开始争抢锁标记。
- notifyAll()
- 通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。
- 被唤醒的线程,进⼊到锁池,开始争抢锁标记。
wait和sleep的区别
- sleep()方法,在休眠时间结束后,会自动的被唤醒。而wait()进入到的阻塞态,需要被notify/notifyAll手动唤醒。
- wait()会释放自己持有的指定的锁标记,进入到阻塞态。sleep()进入到阻塞态的时候,不会释放自己持有的锁标记。
public class Test {
public static void main(String[] args) {
Runnable runnableA=()->{
synchronized ("a"){
System.out.println("线程A获取了a锁,等待获取b锁");
synchronized("b"){
System.out.println("线程A同时获取了a锁和b锁");
// 当 "b" 锁使用结束之后,通知另外⼀个线程使用结束了
"b".notify();
}
// 当 "a" 锁使用结束之后,通知另外⼀个线程使用结束了
"a".notify();
}
};
Runnable runnableB=()->{
synchronized ("b"){
System.out.println("线程B获取了b锁,等待获取a锁");
try {
// 释放自己持有的 "b" 锁标记,进入等待队列
"b".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized("a"){
System.out.println("线程B同时获取了b锁和a锁");
}
}
};
new Thread(runnableA,"A").start();
new Thread(runnableB,"B").start();
}
}
public class Test {
public static void main(String[] args) {
Runnable runnableA=()->{
synchronized ("a"){
System.out.println("线程A获取了a锁,等待获取b锁");
try {
// 让当前线程释放自己持有的 "a" 锁
// 进入到等待队列
"a".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized("b"){
System.out.println("线程A同时获取了a锁和b锁");
// 当 "b" 锁使用结束之后,通知另外⼀个线程使用结束了
}
}
};
Runnable runnableB=()->{
synchronized ("b"){
System.out.println("线程B获取了b锁,等待获取a锁");
synchronized("a"){
System.out.println("线程B同时获取了b锁和a锁");
// 唤醒等待队列中,一个等待 "a" 锁的线程
"a".notify();
}
}
};
new Thread(runnableA,"A").start();
new Thread(runnableB,"B").start();
}
}
练习1
实例化两个线程,同时对一个数字进行操作。一个线程对这个数字进行加1,另外一个线程对这个数字进行减一。输出每一次的操作之后的这个数字的值。
class Program {
private static int number = 10;
public static void main(String[] args) {
Runnable runnable1 = () -> {
while (true) {
System.out.println(Thread.currentThread().getName() + "对数字加1,结果是: " + ++number);
}
};
Runnable runnable2 = () -> {
while (true) {
System.out.println(Thread.currentThread().getName() + "对数字减1,结果是: " + --number);
}
};
Thread thread0 = new Thread(runnable1, "加线程");
Thread thread1 = new Thread(runnable2, "减线程");
thread0.start();
thread1.start();
}
}
练习2
有五个人同时过一个独木桥,一个独木桥同时只能允许一个人通过。每一个人通过独木桥的时间是随机在 [5,10] 秒,输出这个独木桥上每一个人的通过详情,例如:张三开始过独木桥了... 张三通过独木桥了!
public static void main(String[] args) {
// 有五个人同时过一个独木桥,一个独木桥同时只能允许一个人通过。每一个人通过独木桥的时间是随机在 [5,10] 秒,输出这个独木桥上每一个人的通过详情,例如:张三开始过独木桥了... 张三通过独木桥了!
Runnable runnable = () -> {
synchronized ("") {
// 模拟人通过桥的情况
System.out.println("【" + Thread.currentThread().getName() + "】开始过独木桥了...");
Random random = new Random();
double time = Math.random() * 5 + 5;
try {
Thread.sleep((long)(time * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【" + Thread.currentThread().getName() + "】已经通过独木桥了,用时: " + time + "秒");
}
};
Thread thread1 = new Thread(runnable, "小明");
Thread thread2 = new Thread(runnable, "小美");
Thread thread3 = new Thread(runnable, "小娟");
Thread thread4 = new Thread(runnable, "小灰");
Thread thread5 = new Thread(runnable, "老王");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
练习3
小明、小美、小娟共用一张银行卡,其中,小明每次向银行卡里面存 [1000,2000] 块钱,小美、小娟每次从银行卡里面取 [300, 600] 块钱。当卡里没钱的时候,会通知小明存钱。小明存完钱之后,通知他们两个人取钱。设计程序,实现这个场景。
public class Three {
public static void main(String[] args) {
Random random = new Random();
BankCard card = new BankCard();
Thread xiaoming = new Thread(() -> {
while (true) {
card.saveMoney(random.nextInt(1000) + 1000);
}
}, "小明");
Runnable runnable = () -> {
while (true) {
card.getMoney(random.nextInt(300) + 300);
}
};
xiaoming.start();
new Thread(runnable, "小美").start();
new Thread(runnable, "小娟").start();
}
/**
* 银行卡类
*/
private static class BankCard {
// 钱的数量
private int money;
/**
* 将指定的金额存入到银行卡中
* @param money 需要存的金额
*/
public synchronized void saveMoney(int money) {
this.money += money;
System.out.println("小明向银行卡中存了" + money + "元,剩余数量" + this.money + "元");
// 存完钱
this.notifyAll();
// 等待下次存钱
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 从银行卡取钱
* @param money 希望取的钱
*/
public synchronized void getMoney(int money) {
// 找到最小值
money = Math.min(money, this.money);
// 取钱
this.money -= money;
System.out.println("【"+Thread.currentThread().getName()+"】取了【"+money+"】元,剩余: 【"+this.money+"】");
if (this.money <= 0) {
// 通知小明存钱
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}