多线程(二、线程安全,线程通讯,线程池)
经过前面的学习,我们发现使用多线程中会有很多问题,这就涉及到了每个线程之间的配合问题。所以我们继续学习线程同步、线程通讯和线程池。
线程安全问题
线程安全问题是多线程的一个重点问题。
经典的卖票问题:
public class Test7 {
public static void main(String[] args) {
Station station = new Station();
Thread thread1 = new Thread(station,"窗口一");
Thread thread2 = new Thread(station,"窗口二");
Thread thread3 = new Thread(station,"窗口三");
Thread thread4 = new Thread(station,"窗口四");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
class Station implements Runnable{
private int number = 1;
private boolean flag = true;
@Override
public void run() {
while (flag) {
buy();
}
}
public void buy(){
if (number<=20){
System.out.println(Thread.currentThread().getName() + "卖出一张票," + number + "号");
number++;
}else {
flag = false;
}
}
}
运行上面代码就可以发现,不同的窗口卖出了同一张车票的情况,这就是线程不安全的情况。
线程同步
线程同步也就是线程安全,多线程只要牵扯到操作成员变量的情况,就可能发生线程安全问题
首先,操作共享数据的代码这些代码同时只让一个线程进行,其他线程运行到这行代码就让等待,只有线程把这些代码执行完毕,才会让给下一个线程执行。
Java中引入了同步监视器来解决这个问题,同步监视器也有两种使用方式,同步代码块和同步方法,
(JDK5中新增了lock锁方法,会在后面学习)
同步代码块
synchronized(同步监视器对象){
//同步代码···
}
线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器的锁定
-
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程会自动释放对同步监视器对象的锁定
-
Java程序运行使用任何对象来作为同步监视器对象,只要保证共享资源的这几个线程锁的是同一个同步监视器对象即可
使用this作为同步监视器对象
如果我们使用Runnable接口实现线程,那么如果两个线程共用一个Runnable接口实现类对象作为target的话,那么我们把我们的同步监视器对象换成this也可以,锁住这一个实现类,也可以达到线程安全的效果
(如果线程是继承Thread类实现的,同步监视器对象就不能用this,因为此时this代表的是他们自己new的线程)
public void buy(){
==synchronized (this) {==
if (number <= 20) {
System.out.println(Thread.currentThread().getName() + "卖出一张票," + number + "号");
number++;
} else {
flag = false;
}
==}==
}
使用共享资源作为同步监视器对象
这时,我们使用继承Thread类来创建线程,那么就要锁住这个共同使用的station对象
public class Test3 {
public static void main(String[] args) {
Station station = new Station();
SThread sThread1 = new SThread(station);
SThread sThread2 = new SThread(station);
sThread1.start();
sThread2.start();
}
}
class Station{
private int number=1;
private boolean flag = true;
public void buy(){
while (flag) {
if (number <= 10) {
System.out.println(Thread.currentThread().getName() + "卖出一张票," + number + "号");
number++;
} else {
flag = false;
}
}
}
}
class SThread extends Thread{
Station station;
public SThread(Station station){
this.station=station;
}
@Override
public void run() {
//锁住共享的对象
synchronized (station) {
station.buy();
}
}
}
同步方法
同步方法就是使用synchronized关键字来修饰的方法。
对于同步方法而言,无须显示指定同步监视器:
- 静态方法的同步监视器对象是当前类的Class对象,
- 非静态方法的同步监视器对象是调当前方法的this对象。
//把这里的买票的方法使用synchronized修饰,那么它就是同步方法了
public ==synchronized== void buy(){
while (flag) {
if (number <= 10) {
System.out.println(Thread.currentThread().getName() + "卖出一张票," + number + "号");
number++;
} else {
flag = false;
}
}
}
释放同步监视器的锁定
会释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步方法、同步代码块中遇到了break、return终止了该代码块或方法的执行
- 当前线程在同步方法、同步代码块中出现了未处理的Exception或Error,导致当前线程异常结束
- 当前线程在同步方法、同步代码块中执行了锁对象的wait()方法,当前线程被阻塞
不会释放锁对象的操作:
- 当线程线程执行同步方法、同步代码块时,程序调用了Thread.sleep()、Thread.yield()方法暂停当前线程的执行,
- 当线程线程执行同步方法、同步代码块时,其他程序调用了该线程的suspend()方法,该线程挂起。(应该尽量避免使用suspend()和resume()这样已经过时的方法来操作线程)
死锁
同步可以保证资源共享操作的正确性,但是过多的同步也会产生问题。
当两个线程,同时拿到对方需要的同步监视器对象,并且等待拿到自己需要的同步监视器对象时,就形成了死锁。发生死锁后,程序不会发生异常,也不会有任何提示,两个所有线程处于阻塞状态,都无法继续。
我们这里可以用买卖商品来重现这个问题,当卖家有货时,他要拿到钱才给货,小明想要买货,但是他想拿到货再给钱,他们两个谁都不让步,就僵持在这里了。
public class Test1 {
public static void main(String[] args) {
Object goods = new Object();
Object money = new Object();
Thread selle = new Thread(new Selle(goods,money),"卖家");
Thread xiaoming = new Thread(new XiaoMing(goods,money),"小明");
selle.start();
xiaoming.start();
}
}
class Selle implements Runnable{
Object goods;
Object money;
public Selle(Object goods,Object money){
this.goods=goods;
this.money=money;
}
@Override
public void run() {
synchronized (goods) {
System.out.println(Thread.currentThread().getName()+":我有货,先给钱");
synchronized (money){
System.out.println("得到钱,给货。。");
}
}
}
}
class XiaoMing implements Runnable{
Object goods;
Object money;
public XiaoMing(Object goods,Object money){
this.goods=goods;
this.money=money;
}
@Override
public void run() {
synchronized (money) {
System.out.println(Thread.currentThread().getName()+":我有钱,先给货");
synchronized (goods){
System.out.println("得到货,给钱。。");
}
}
}
}
线程通信
常用方法:Object类中
返回值 | 方法名 | 说明 |
---|---|---|
void | wait() | 使当前线程等待,直到有其他方法调用notify或者notifyAll |
void | wait(long timeout) | 使当前线程等待,直到有其他方法调用notify或者notifyAll,或者指定的等待时间已到 |
void | notify() | 唤醒正在等待的一个线程 |
void | notifyAll() | 唤醒正在等待的所有线程 |
!这三个方法只能用在同步代码块或者同步方法中
生产者消费者模式
生产者和消费者模式(Producer-consumer problem),也称为有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例
该问题描述了两个(多个)共享固定大小缓冲区的线程(即生产者和消费者)实际运行时会发生的问题,生产者产生一定量的数据放到缓冲区中,然后一直重复这个过程;同时 ,消费者在缓冲区中消耗这些数据,该问题就是要保证,生产者在缓冲区满时不会继续生产,消费者也不会在缓冲区为空时消耗数据,
public class Test9 {
public static void main(String[] args) {
Workbench workbench = new Workbench();
Cook cook1 = new Cook(workbench,"C一号");
Water water1 = new Water(workbench,"F一号");
Cook cook2 = new Cook(workbench,"C二号");
Water water2 = new Water(workbench,"F二号");
cook1.start();
water1.start();
cook2.start();
water2.start();
}
}
class Workbench {
public static final int MAX_NUM = 10;
public int num;
//往工作台上放快餐
public synchronized void put(){
//当工作台满了,就让调用这个方法的厨师线程等待一下
while (num>=MAX_NUM){
try {
System.out.println("工作台放满了,厨师等待···");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println(Thread.currentThread().getName()+"厨师制作了一份快餐,此时工作台上有"+num+"份");
this.notifyAll();
}
//从工作台取餐
public synchronized void take(){
//当工作台没有快餐时,让调用这个方法的服务员等待一下
while (num<=0){
try {
System.out.println("没有快餐,服务员等待···");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
System.out.println(Thread.currentThread().getName()+"服务员取走了一份快餐,此时工作台上有"+num+"份");
this.notifyAll();
}
}
class Cook extends Thread{
private Workbench workbench;
public Cook(Workbench workbench,String name){
super(name);
this.workbench=workbench;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
workbench.put();
}
}
}
class Water extends Thread{
private Workbench workbench;
public Water(Workbench workbench,String name){
super(name);
this.workbench=workbench;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
workbench.take();
}
}
}
线程池
前面的学习中,我们每完成一个线程任务就创建一个线程,这个线程执行完毕后,就被销毁了。下次执行其他任务还要继续创建线程,在实际使用中,创建和销毁一个线程的开销是很大的,每一个活动的线程都会消耗一定的系统资源。
线程池其实就是一个容纳多个线程的容器,其中的线程可以被重复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
(线程池中有很多操作都是与优化资源相关的)
合理使用线程池的好处:
- 降低资源的消耗,减少了创建和销毁线程的次数
- 提高响应速度,当任务到达时,任务可以不需要等待线程线程创建就可以立即执行
- 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止同时创建多条线程消耗过多内存导致服务器宕机
应用:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ExecutorService 线程池对象
* Executors中的方法可以返回各种各样的线程池对象
*/
public class Test12 {
public static void main(String[] args) {
//创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);
//创建Runnable实现类对象
MYRunnable r1 = new MYRunnable();
//从线程池中获取一个线程来执行run中的任务
service.submit(r1);
service.submit(r1);
service.submit(r1);
service.submit(r1);
//关闭线程池
service.shutdown();
}
}
class MYRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行线程任务中··");
}
}