一、简介
当多个线程同时访问共享数据时可能会出现问题,称为线程安全问题。
当多线程访问共享数据时,由于CPU的切换,导致一个线程只执行了关键代码的一部分,还没执行完 ,此时另一个线程参与进来,会导致共享的数据发生异常。
二、线程的同步机制synchronized + 锁
2.1 简介
同步:排队执行 , 效率低但是安全。
异步:同时执行 , 效率高但是数据不安全。
锁,也称为对象锁,每个对象都自带一个锁(标识),且不同对象的锁是不一样的 。
2.2 同步代码块和同步方法
2.2.1 同步的前提
- 必须有两个或两个以上的线程
- 必须是多个线程使用同一资源
- 必须保证同步中只能有一个线程在运行
2.2.2 线程同步执行过程
执行过程:
-
当线程执行同步代码块或同步方法时,必须获取特定对象的锁才行 ,且一旦对象的锁被获取,则该对象就不再拥有锁,直接线程执行完同步代码块或同步方法时 才会释放对象的锁 。
-
如果线程无法获取特定对象上的锁,则线程会进入该对象的锁池中等待,直到锁被归还对象,此时需要该锁的线程会进行竞争 。
2.2.3 同步代码块
被synchronized包围的代码块,称为同步代码块 。
卖票案例:
public class TicketRunnable implements Runnable {
//要卖的票数
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//需要同步的代码块
synchronized (this){//通常将当前对象作为同步对象
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}
}
}
}
public static void main(String[] args) {
TicketRunnable ticket = new TicketRunnable();
Thread t1 = new Thread(ticket,"zhangsan");
Thread t2 = new Thread(ticket,"lisi");
Thread t3 = new Thread(ticket,"wangwu");
Thread t4 = new Thread(ticket,"zhaoliu");
t1.start();
t2.start();
t3.start();
t4.start();
}
2.2.4 同步方法
被synchronized修饰的方法,称为同步方法。
卖票案例:
public class TicketRunnable implements Runnable {
//要卖的票数
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.sale();
}
}
//同步方法
public synchronized void sale() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}
}
public static void main(String[] args) {
TicketRunnable ticket = new TicketRunnable();
Thread t = new Thread(ticket, "zhang");
Thread t2 = new Thread(ticket, "li");
Thread t3 = new Thread(ticket, "wang");
Thread t4 = new Thread(ticket, "zhao");
t.start();
t2.start();
t3.start();
t4.start();
}
}
2.3 线程同步小结
1.同步监视器
synchronized(obj){}中的obj称为同步监视器。
同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器。
同步方法中无需指定同步监视器,因为同步方法的监视器是this,也就是该对象本身。
2.同步监视器的执行过程
第一个线程访问,锁定同步监视器,执行其中代码。
第二个线程访问,发现同步监视器被锁定,无法访问。
第一个线程访问完毕,解锁同步监视器。
第二个线程访问,发现同步监视器未锁,锁定并访问。
三、线程间的通信
3.1 锁池和等待池
每个对象都自带锁池和等待池 。
1.锁池:
当线程执行synchronized块时如果无法获取特定对象上的锁,此时会进入该对象的锁池 。
当锁被归还给该对象时,锁池中的多个线程会竞争获取该对象的锁 。
获取对象锁的线程将执行synchronized块,执行完毕后会释放锁。
2.等待池:
当线程获取对象的锁后,可以调用 wait() 方法放弃锁,此时会进入该对象的等待池 。
当其他线程调用该对象的 notify() 或 notifyAll() 方法时,等待池中的线程会被唤醒,会进入该对象的锁池 。
当线程获取对象的锁后,将从它上次调用wait()方法的位置开始继续运行。
3.2 线程通信相关的方法
方法名称 | 描述 |
---|---|
final void wait() | 表示线程一直在等待状态,直到其它线程唤醒 |
final void wait(long timeout) | 线程等待并可以指定等待的时间,单位为毫秒 |
final void wait(long timeout,int namos) | 线程等待,可以指定毫秒、微秒时间 |
final void notify() | 唤醒一个在等待的线程 |
final void notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程先执行 |
注意:这几个方法都只能在同步方法或同步代码块中使用(synchronized),即只有获取了锁的线程才能调用 。
等待和唤醒必须使用的是同一个对象。
3.3 案例演示
生产者与消费者:
商品类:
public class Goods {
//商品品牌
private String brand;
//商品名称
private String name;
//用来表示是否存在商品,true表示存在
private boolean flag =false;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//生产者生产商品方法
public synchronized void productGoods(String brand,String name){
/**
* 当生产者抢占到cpu资源之后会判断当前对象是否有值,
* 如果有的话,意味着消费者还没有消费。需要提醒消费者去消费,
* 同时当前线程会进入阻塞状态,等待消费者取走商品后,再次生产。
* 如果没有的话,不需要等待,也不需要进入到阻塞状态,直接生产商品即可
*/
if (flag){
try {
wait();
System.out.println("商品还没生产哦,请稍等片刻。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setBrand(brand);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了:" + this.brand + "--" + this.getName());
//代码执行到这,表示已生产了商品,需要将flag的值改变
flag = true;
//去唤醒消费者去消费商品
notify();
}
//消费者消费商品方法
public synchronized void getGoods(){
/**
* 如果flag为false的话,意味着生产者还没有生产有商品,
* 此时消费者还无法进行消费,需要让消费者线程进入到阻塞状态,等待生产者生产商品
* 当有商品后,再开始消费
*/
if (!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者取走了:" + this.getBrand() + "-->" + this.getName());
//修改flag 的值
flag = false;
//唤醒生产者去生产商品
notify();
}
}
生产者类:
public class Producer implements Runnable{
private Goods goods;
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
goods.productGoods("农夫山泉", "矿泉水");
} else {
goods.productGoods("康师傅", "泡椒牛肉面");
}
}
}
}
消费者类:
public class Consumer implements Runnable {
private Goods goods;
public Consumer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
goods.getGoods();
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
//创建商品对象
Goods goods = new Goods();
//创建消费者对象,并将共享数据对象传入
Consumer consumer = new Consumer(goods);
//创建生产者对象,并将共享数据对象传入
Producer producer = new Producer(goods);
//调用生产者与消费者线程
new Thread(consumer).start();
new Thread(producer).start();
}
}