线程(二)如何实现多线程?偷偷学习,默默变强,搞事情?小明又在学习了,快来阻止他,Thread,Runnable
线程不安全问题:确保变量在被一个线程访问的同时不被其他线程访问,这种措施路段线程的同步。
同步的关键是管程,管程(操作系统里的,同学们认真学啊😭,虽说学起来确实让人伤心,小明这老废物上操作系统课时天天开飞机,所以可能理解的会不太恰当,多去看看大佬写的,不过开飞机确实爽啊)是一个互斥独占锁定的对象(或者说是将一些使线程操作安全的语句模块封装在对象里,是互斥的),在给定的时间,仅有一个线程可以获得管程,当一个线程进入管程后,其他尝试进入该管程的所有线程必须挂起直到拥有管程的线程退出管程
解决方案1. 同步代码块
同步语句的同步作用域是synchronized关键字后大括号内的部分,该区域内的代码块构成一个管程,即一个互斥独占锁定的对象。当有线程进入该管程后,其他任何尝试进入该管程的线程必须挂起直到占用管程的线程退出管程,由于同一时刻只能有一个线程进入该语句块,因此该语句块实现了对成员方法的线程同步访问。临界代码段即是多线程访问共享资源和数据的那一段代码。
使用同步语句可以将部分代码进行同步管理而并非一定是一个方法,这样可以提高多线程的工作效率,因其对象可以是任意对象,所以如果需要同步的代码块里没有设计明显的操作对象,可以设定一个空的字符串对象或者Object对象等代替。如果临界代码访问共享变量,锁定对象通常是this。
格式:synchronized(){}
synchronized(锁对象(任何对象都可以传入,任何对象都可以打上锁标记)){ 不同线程要锁住同一对象,线程观察传入对象是否打上锁的标记(底层机制)打上标记意味着有人执行,就等待其解锁,抢到解锁的对象的线程给对象打上标记,要锁同一把锁才有用,即同一个对象}
(手里握住了真理) 注释代码为未锁之前(只要在最后一张票卖完之前进入程序就会出现数据问题)重点和需要注意的地方都用了文字注释(try catch 和 Thread.sleep();等方法不是必要,只是为了让每个线程都有机会售出车票)
//线程不安全
public static void main(String[] args) {
Object o = new Object();
Runnable run = new Ticket();//多态
new Thread(run).start();//给他命名 甲
new Thread(run).start();//乙
new Thread(run).start();//丙
}
static class Ticket implements Runnable {
// 票数
private int count = 10;
/*@Override //不安全 没锁之前
public void run() {//1.假设一个极限情况 count = 1;
while (count > 0) {//2.甲乙丙三人抢到时间片,甲乙丙到这(只要没进行count--操作,都能进来)
System.out.println("正在准备卖票");
try {//有没有休眠都可能出问题
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}//3. 三个线程都进来了,都会执行count-- 最终count为-2(这就是线程不安全问题)
count--;
System.out.println("出票成功,余票:" + count);
}
}*/
private Object o = new Object();
@Override
public void run() {
//不能在这创建对象,这样相当于不同锁,因为每个线程进来都会创建一个对象(不同的对象),一样会不安全
// Object o = new Object();
while (true) {
synchronized (o) {
if (count > 0) {
System.out.println("正在准备卖票");
count--;
System.out.println(Thread.currentThread().getName() + ":" + count);
}
}
}
}
}
解决方案2. 同步方法
把需要同步的代码单独抽离出来封装成方法,当一个线程正在调用一个同步方法时,所有试图调用该方法或者其他同步方法的同实例的其他线程必须等待,直到拥有管程的线程从所调用的同步方法中返回,对象的控制权才能交给其他等待的线程。
其实java的每个对象都有一个对象锁,也被称为监视器。当一个线程访问某个对象的同步方法时,就锁定该对象,在此期间,其他任何线程都不能访问该对象的任何同步方法。直到之前的线程执行完毕才将该对象锁释放掉,其他线程才有机会去访问该对象的同步方法。
public void run() {
while (true) {
boolean flag = sale();
if (!flag) {
break;
}
}
}
public synchronized boolean sale() {
if (count > 0) {
System.out.println("正在准备卖票");
count--;
System.out.println(Thread.currentThread().getName() + ":" + count);
return true;
}
return false;
}
同步控制的几点注意
- synchronized语句块中的的代码应该越少越好,否则会丧失多线程的并发运行的优势。
- 任何时刻,一个管程只能被一个线程所占有,只有等线程运行完其所占用的管程对象的全部管程synchronized代码或方法后,线程才会释放该管程。
- 临界资源中的共享成员应该定义为private。
- 当某个实例对象的同步方法被一个线程所占用时,其他线程就不允许再进入该同一实例的其他任何同步方法了。
- 因为线程可以阻塞,并且同步控制可以全一些线程处于等待某个线程释放共享资源,这就容易产生死锁状态(线程第一篇里有讲),所以在设计时一定要防止线程出现死锁。
显式锁和隐式锁
之前的同步代码块和同步方法都是隐式锁(我的理解就是看不到他如何加锁,可以理解为家里大门的自动锁,不需要动手)
而显式锁就是能看到他加锁(就是锁门有插钥匙的动作,锁好后再拔出钥匙)
解决方案3. 显式锁 Lock(接口) 及其常用子类 ReentrantLock
就是在需要线程安全的代码段前加锁,段后解锁
(还是得强调java 默认非公平锁:抢就完了)
//fair公平:传入true 为公平锁 先来先得,每人有份
private Lock l = new ReentrantLock(true);
@Override
public void run() {
while (count > 0) {
l.lock();//出门要锁门
if (count > 0) {
System.out.println("正在准备卖票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
}
l.unlock();//回家要开锁
}
}
多线程通信:生产者与消费者问题。
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
解决办法
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
(以上百度的)
我的理解是生产者生产出产品,然后消费者消费产品,而生产者生产产品时,消费者必需等待(因为没有产品消费),反之也是如此,但是java是非公平锁,哪个线程抢到就是哪个线程执行,所以会出现这个情况,即:生产者生产完产品,他没让消费者消费,而又接着生产(给他颁发劳模奖),或者消费者消费完又消费(你喝西北风呢),别说这种概率还很大呢。
或者这么说吧,生产者和消费者顺序操作产品,但他们不知道对方对其做了什么操作,就有点像之前卖票,但又不一样,假设生产者生产产品,消费者消费产品,这一过程要重复多次,这就必需要轮流来(多个线程要先生产后消费,而且对顺序严格要求,循环往复),而卖票只要有线程卖就行(多个线程只管卖票),(不知道是否解释的更复杂了)。
解决方法:
生产者生产时,让消费者睡觉(等待),生产好产品后,唤醒消费者,然后自己等待,这时消费者消费,等消费者消费完,唤醒生产者,然后自己等待,可以加循环,多次重复。
可能会有小伙伴有疑问,为什么有同步方法不能解决这个问题?
不公平锁 方法结束,厨师又可以抢着做饭 服务员同理,他们都操作食物,而食物有两个方法,这两个方法不同步,但又要分开,因为是不同对象调用,厨师调用setNameAndTaste方法做菜,服务员调用get方法端菜,两个方法不同步,所以让他们同一时间只能有一个人执行方法。
解决方案:加标记flag判断,让线程轮换 厨师做完唤醒服务员,自己等待,服务员同理,代码的核心部分就是食物类里的两个线程,简单来说就是限制这两个方法,让他们同一时刻只有一个方法执行。
(代码如下:为了观看效果厨师和服务员都为内部类,建议单独建类)
public static void main(String[] args) {
Food f = new Food();
new Chef(f).start();
new Waiter(f).start();
}
static class Chef extends Thread {//厨师,生产者
private Food f;
public Chef(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 3 == 0) {
f.setNameAndTaste("重庆火锅", "麻辣味");
} else if (i % 3 == 1) {
f.setNameAndTaste("剁椒鱼头", "香辣味");
} else if (i % 3 == 2) {
f.setNameAndTaste("糖醋排骨", "酸甜味");
}
}
}
}
static class Waiter extends Thread {//服务生,消费者
public Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
f.get();
}
}
}
static class Food{ //食物
private String name;
private String taste;
//标记,表示可以开始做饭
private boolean flag = true;
public synchronized void setNameAndTaste(String name, String taste) {
if (flag) {
this.name = name;
this.taste = taste;
flag = false;
this.notifyAll();//唤醒所有等待(对象的)线程
try {
this.wait();//线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get() {
if (!flag) {
System.out.println("服务员端走的名称是:" + name + ", 味道是:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
图片来源网络