一、问题引出
多个线程访问同一个资源时,如果操作不当就很容易产生意想不到的错误,比如常见的抢票程序:
public class Demo1 {
public static void main(String[] args) {
Ticket tt = new Ticket();
new Thread(tt, "甲").start();
new Thread(tt, "乙").start();
}
}
class Ticket implements Runnable {
private int ticketCount = 10;
@Override
public void run() {
while (ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第【" + (10 - ticketCount + 1) + "】张火车票");
ticketCount--;
}
}
}
结果:
乙抢到了第【1】张火车票
甲抢到了第【2】张火车票
乙抢到了第【3】张火车票
甲抢到了第【4】张火车票
乙抢到了第【5】张火车票
甲抢到了第【6】张火车票
乙抢到了第【7】张火车票
甲抢到了第【8】张火车票
乙抢到了第【9】张火车票
甲抢到了第【9】张火车票
乙抢到了第【11】张火车票
以上代码可以看出,第9张票被两个人抢,且出现了第11张票,这不符合实际。为什么会出现这种现象?原因是当乙抢到第9张票但还未执行ticketCount–;语句时,甲也进入了run()方法,此时票数仍然是第9张票,也打印出了第9票,因此第9张票被抢了两遍。之后甲和乙都会执行ticketCount–;语句,当ticketCount =0时,无论哪个线程抢到资源都会打印抢到了第【11】张火车票,当ticketCount = -1时,停止。
综上分析,问题出现的主要原因就是甲、乙两个线程同时访问同一资源,造成了资源污染。
二、线程同步
上面可知当多个线程同时访问同一资源,就可能会造成了资源污染,解决这个问题的方法就是在某个线程访问资源时,其他线程在资源或者方法外面等待,也就是线程同步,或者叫加锁。
线程同步就是指多个操作在同一时间段内只能有一个线程进行,而其他线程要等待此线程完成之后才可以继续进行。
要实现线程同步,需要通过关键字synchronized关键字,利用这个关键字可以定义同步方法或者代码块。格式如下:
synchronized(同步对象){
操作;
}
一般要进行同步对象处理时,采用当前对象this进行同步。上面的抢票代码加上同步后如下:
public class Demo1 {
public static void main(String[] args) {
Ticket tt = new Ticket();
new Thread(tt, "甲").start();
new Thread(tt, "乙").start();
}
}
class Ticket implements Runnable {
private int ticketCount = 10;
@Override
public void run() {
synchronized(this){
while (ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第【" + (10 - ticketCount + 1) + "】张火车票");
ticketCount--;
}
}
}
}
结果:
甲抢到了第【1】张火车票
甲抢到了第【2】张火车票
甲抢到了第【3】张火车票
甲抢到了第【4】张火车票
甲抢到了第【5】张火车票
甲抢到了第【6】张火车票
甲抢到了第【7】张火车票
甲抢到了第【8】张火车票
甲抢到了第【9】张火车票
甲抢到了第【10】张火车票
加锁或者同步处理以后,虽然多线程同时访问同一资源的问题虽然解决了,但是线程同步会降低整体性能。
三、死锁
死锁就是多个线程间相互等待的状态。
class MyThread implements Runnable{
int flag = 1;
// 必须是静态资源
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
System.out.println("flag= " + flag);
if(flag == 1){
synchronized (o1){
System.out.println(Thread.currentThread().getName() + "我抢到了o1,还需要o2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("111");
}
}
}
if(flag == 0){
synchronized (o2){
System.out.println(Thread.currentThread().getName() + "我抢到了o2,还需要o1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("222");
}
}
}
}
}
public class DeadLock {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
// 设置线程1先抢占o1
myThread1.flag = 1;
// 设置线程1先抢占o2
myThread2.flag = 0;
Thread t1 = new Thread(myThread1);
Thread t2 = new Thread(myThread2);
t1.start();
t2.start();
}
}
有两个线程,都需要锁住同样的2个对象(a、b)才能完成操作,其中线程1已经锁住了a对象,线程2锁住了b对象,两个线程都不释放锁就会造成死锁。程序无法停止。
四、生产者和消费者模式
生产者和消费者模式是用来解决死锁问题的。
为什么可以解决呢?
生产者和消费者模式中,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能,需要根据信号来生产或获取:当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来。这里实际体现了信号灯法:flag = true,生产者生产,消费者等待;反之,生产者等待,消费者生产。
下面以蒸包子和吃包子为例,厨师作为生产者输出包子,吃包子的作为消费者,如果没有采用生产者消费者模式,那就可能出现吃包子的没包子吃或者蒸包子的产能过剩的情况;而采用生产者消费者模式后,厨师蒸好包子后不是接着蒸,而是把蒸好的包子放在篮子里然后停下来告诉消费者可以吃了,然后消费者接到信号开始从篮子里拿包子吃,直到吃完后会给厨师一个信号,告诉他可以接着蒸包子了,这时消费者停下,厨师开始蒸包子,接着循环下去。
/**
* 装包子的篮子类
*/
class Container{
private String baozi;
// 篮子状态信号标志:flag = true,篮子空了,生产者生产;flag = false,篮子满了,消费者消费
private boolean flag = true;
// 模拟生产过程,生产过程要加锁,防止生产过程中消费者过来消费
public synchronized void play(String baozi) throws InterruptedException {
// 生产者等待
if(!flag){
this.wait();
}
// 生产者生产
Thread.sleep(500); // 模拟生产耗时
// 包子蒸好了
this.baozi = baozi;
System.out.println("生产" + baozi);
// 通知消费者来吃
this.notify();
// 停止生产
this.flag = false;
}
// 模拟消费过程,消费过程要加锁,防止消费过程中生产者过来生产
public synchronized void eat() throws InterruptedException {
// 消费者等待
if(flag){
this.wait();
}
// 消费者消费
Thread.sleep(100); // 模拟消费耗时
System.out.println("篮子里的包子已经吃完了");
// 通知生产者需要蒸包子了
this.notify();
// 停止吃包子
this.flag = true;
}
}
/**
* 定义生产者
*/
class Player implements Runnable{
private Container container;
public Player(Container container) {
this.container = container;
}
// 单日生产素馅的包子,双日生产肉馅的包子
@Override
public void run() {
for (int i = 0; i < 6; i++) {
if(i%2 == 0){
try {
container.play("肉馅的包子");
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
try {
container.play("素馅的包子");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 定义消费者
*/
class Consumer implements Runnable{
private Container container;
public Consumer(Container container) {
this.container = container;
}
// 不管啥馅的包子我都吃
@Override
public void run() {
for (int i = 0; i < 6; i++) {
try {
container.eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class CpDemo {
public static void main(String[] args) {
// 同一个篮子
Container container = new Container();
Player player = new Player(container);
Consumer consumer = new Consumer(container);
new Thread(player, "生产者").start();
new Thread(consumer, "消费者").start();
}
}
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了
以上,生产者线程和消费者线程交替进行,就能很好的避免死锁问题。
五、volatile和synchronized的区别
volatile关键字主要是在属性定义上使用,表示此属性为直接数据操作,而不进行副本的拷贝处理。
volatile和synchronized的区别:
- volatile主要是在属性定义上使用,而synchronized是在代码块或方法上使用
- volatile无法描述同步处理,是一种直接内存处理,避免了副本操作;synchronized是同步操作。
六、sleep和wait的区别
① 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
② 锁: sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中,使得其他线程可以使用同步控制块或者方法。
sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
③ 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
七、notify和notifyAll的区别?
先要理解锁池和等待池:
- 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
- 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
notify和notifyAll的区别
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 - notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。