文章目录
- 一、线程引入
- 1、并行与并发
- 2、进程和线程
- 3、主线程
- 二、多线程
- 1、使用Thread实现多线程
- 2、Thread类简单方法
- (1)设置获取线程的名称
- (2)sleep方法
- 3.Runnable接口实现多线程
- 4、线程安全问题
- (1)卖票案例问题代码
- (2)同步代码块synchronized
- (3)同步方法synchronized
- (4)静态的同步方法static synchronized
- (5)使用Lock锁
- 5、等待与唤醒
- (1)进程状态图
- (2)Demo01WaitAndNotify (once做吃)
- (3)唤醒单个Demo01WaitAndNotifyWhileTrue (while做吃)
- (4)唤醒全部Demo02WaitArgsAndNotifyAll (while做吃)
- (5)案例:做不同种类的包子
- 6、线程池(POOL)
- 三、多线程相关的面试题
- 1、线程和进程的区别?
- 2、一个Java应用程序至少有几个线程?
- 3、如何停止一个线程?
- 4、sleep() 和 wait() 有什么区别?
- 5、多线程的创建方式?
- 6、谈谈你对线程池的理解?jdk提供了哪几种线程池?他们有什么区别?
- 7、说一下ThreadPoolExecutor各个参数的含义?
- 8、说一下线程的生命周期?
- 9、启动一个线程是调用 run() 方法还是 start() 方法?
- 10、什么情况下导致线程死锁,遇到线程死锁该怎么解决?
- 11、什么是乐观锁和悲观锁?
- 12、乐观锁一定就是好的吗?
- 13、说一下线程池的启动策略?
- 14、请说出同步线程及线程调度相关的方法?
- 15、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
- 16、Synchronized的原理是什么?
- 17、为什么说Synchronized是非公平锁?
- 18、JVM对java的原生锁做了哪些优化?
- 19、Synchronized和 ReentrantLock的异同?
- 20、volatile关键字的作用?
- 21、说一下volatile关键字对原子性、可见性以及有序性的保证?
- 22、什么是CAS?
- 23、什么是AQS?
- 24、Semaphore是什么?
一、线程引入
1、并行与并发
2、进程和线程
进程:
具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
线程:
是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
特点:
- 线程的划分尺度小于进程,这使多线程程序拥有高并发性,
- 进程在运行时各自内存单元相互独立,线程之间内存共享,
- 这使多线程编程可以拥有更好的性能和用户体验
注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。
3、主线程
主线程:执行主(main)方法的线程
单线程程序:java程序中只有一个线程
执行从main方法开始,从上到下依次执行
- JVM执行main方法,main方法会进入到栈内存
- JVM会找操作系统开辟一条main方法通向cpu的执行路径
- cpu就可以通过这个路径来执行main方法
- 而这个路径有一个名字,叫main(主)线程
仿造线程
public class Demo01MainThread {
public static void main(String[] args) {
Person p1 = new Person("小强");
p1.run();
//System.out.println(0/0);
//thread "main".ArithmeticException: / by zero
Person p2 = new Person("旺财");
p2.run();
}
}
public class Person {
private String name;
public void run(){
//定义循环执行20次
for(int i=0;i<20;i++){
System.out.println(name+"-->"+i);
}
}
}
二、多线程
1、使用Thread实现多线程
创建多线程程序的第一种方式:创建Thread类的子类
java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
实现步骤:
1.创建一个Thread类的子类
2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
3.创建Thread类的子类对象
4.调用Thread类中的方法start方法,开启新的线程,执行run方法
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行:
当前线程(main线程)
另一个线程(创建的新线程,执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
java程序属于抢占式调度:
哪个线程的优先级高,哪个线程优先执行;
同一个优先级,随机选择一个执行;
主方法
public class Demo01Thread {
public static void main(String[] args) throws InterruptedException {
//3.创建Thread类的子类对象
MyThread mt = new MyThread();
//4.调用Thread类中的方法start方法,开启新的线程,执行run方法
mt.start();
//主线程
for (int i = 0; i <20 ; i++) {
System.out.println("main:"+i);
}
}
}
线程类
//1.创建一个Thread类的子类
public class MyThread extends Thread{
//2.在Thread类的子类中重写Thread类中的run方法,
//设置线程任务(开启线程要做什么?)
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println("run:"+i);
}
}
}
2、Thread类简单方法
(1)设置获取线程的名称
1.使用Thread类中的方法setName()
void setName()设置该线程的名称。
2.创建一个带参数的构造方法,参数传递线程的名称;
调用父类的带参数的构造方法,把线程名称传递给父类,
让父类Thread给子线程起一个名字。
Thread(String name)分配新的Thread对象
线程类
public class MyThread02SetName extends Thread{
public MyThread02SetName() {}
public MyThread02SetName(String name) {
//方式二:把线程名称传递给父类,让父类Thread给子线程起一个名字。
super(name);
}
@Override
public void run() {
//获取线程名称
System.out.println(Thread.currentThread().getName());
}
}
调用线程的主方法Demo02SetThreadName
/**
* 线程的名称:
* 主线程:main
* 新线程:Thread-0,Thread-1,Thread-2
*/
public class Demo02SetThreadName {
public static void main(String[] args) {
//创建Thread类的子类对象
MyThread02SetName mt = new MyThread02SetName();
//方式一:使用Thread类中的方法setName()
mt.setName("线程1");
mt.start();
//方式二:调用带参数构造器,设置ThreadName
new MyThread02SetName("线程2").start();
}
}
(2)sleep方法
public static void sleep(long millis)
- 使当前正在执行的线程以指定的毫秒数暂停(暂时停止运行)。
毫秒数结束之后,线程继续执行。
public class Demo03ThreadSleep {
public static void main(String[] args) throws InterruptedException {
//模拟秒表
for(int i=1;i<=60;i++){
System.out.println(i);
//使用Thread类的sleep方法让程序睡眠1s
Thread.sleep(1000);
}
}
}
3.Runnable接口实现多线程
创建多线程程序的第二种方式:实现Runnable接口
java.lang.Runnable
Runnable接口应由任何类实现,其实例将由线程执行。该类必须定义一个无参数的方法。
java.lang.Thread类的构造方法
Thread(Runnable target):分配一个新的 Thread对象。
Thread(Runnable target, String name):分配一个新的 Thread对象。
实现步骤:
1.创建Runnable接口的实现类。
2.在实现类中重写Runnable接口的run方法,设置线程任务。
3.创建一个Runnable接口的实现类对象。
4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
5.调用Thread类中的start方法,开启新的线程执行run方法
实现Runnable接口创建多线程的好处:
1.避免了单继承的局限性
一个类只能继承一个类,实现了Runnable接口还可以继承其他类。
2.增强了程序的扩展性,降低了程序的耦合性(解耦)
实现Runnable接口的方式,把设置线程任务和开启线程进行了分离(解耦)
设置线程任务:在实现类中,重写了run方法。
开启线程:创建Thread类对象,调用start方法。
好处:Thread中传递不同的实现类,开启不同任务。
public class Demo01Runnable {
public static void main(String[] args) {
//3.创建一个Runnable接口的实现类对象。
RunnableImpl ri = new RunnableImpl();
//4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread thread = new Thread(ri);
//5.调用Thread类中的start方法,开启新的线程执行run方法
thread.start();
//设置主线程任务
for(int i=0;i<=20;i++){
System.out.println("MainName:"+Thread.currentThread().getName());
}
}
}
线程类
//1.创建一个Runnable接口的实现类
public class RunnableImpl implements Runnable{
//2.在实现类中重写run方法,设置线程任务
@Override
public void run() {
for(int i=0;i<=20;i++){
System.out.println("Runnable:"+
Thread.currentThread().getName());
}
}
}
4、线程安全问题
(1)卖票案例问题代码
/**
* 实现卖票案例
*/
public class RunnableImpl00 implements Runnable{
//定义100张票
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票重复
while(true){
//先判断票是否存在
if (ticket>0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName()
+"正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
(2)同步代码块synchronized
/**
* 实现卖票案例
* 出现线程安全问题:
* 卖出了不存在的票和重复的票
* 解决线程安全问题的一种方案:使用同步代码块
* 格式:
* synchronized(锁对象){
* 可能会出现线程安全问题的代码(访问了共享代码块)
* }
* 注意:
* 1.通过代码块中的锁对象,可以使用任意的对象
* 2.但是保证多个线程使用的锁对象时同一个
* 3.锁对象作用:
* 把同步代码块锁住,只让一个线程在同步代码块中执行
*
*/
public class RunnableImpl01 implements Runnable{
//定义100张票
private int ticket = 50;
//创建一个锁对象(保证共享一个对象:必须写在run外边)
Object obj = new Object();
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票重复
while(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步代码块
synchronized (obj){
//先判断票是否存在
if (ticket>0){
//票存在,卖票
System.out.println(Thread.currentThread().getName()
+"正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
}
(3)同步方法synchronized
/**
* 实现卖票案例
* 出现线程安全问题:
* 卖出了不存在的票和重复的票
* 解决线程安全问题的一种方案:使用同步方法
* 使用步骤:
* 1.把访问了共享数据的代码抽取出来,放到一个方法中
* 2.在方法上添加synchronized修饰符
* 格式:
* 修饰符 synchronized 返回值 方法名(参数列表){
* 可能会出现线程安全问题的代码(访问了共享代码块)
* }
*/
public class RunnableImpl02 implements Runnable{
//定义100张票
private int ticket = 500;
//设置线程任务:卖票
@Override
public void run() {
//this:::com.itheima.demo05.ThreadSafe.Synchronized02.RunnableImpl@1540e19d
System.out.println("this:::"+this);
//使用死循环,让卖票重复
while(ticket>0){
payTicket();
}
}
/**
* 定义一个同步方法
* 同步方法也会把方法内部锁住,只让一个线程执行
* 同步方法的锁对象是谁?
* 就是实现类对象 new RunnableImpl也就是this
*
*/
public synchronized void payTicket(){
//synchronized (this){
//先判断票是否存在
if (ticket>0){
//提高安全隐患出现的概率
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName()
+"正在卖第"+ticket+"张票");
ticket--;
}
//}
}
}
(4)静态的同步方法static synchronized
public class RunnableImpl03 implements Runnable{
//定义100张票
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票重复
while(ticket>0){
payTicketStatic();
}
}
/**
* 静态的同步方法
* 1.同步方法的锁对象是谁?
* 锁是本类的class属性--->class文件对象(反射)
* 2.不能是this
* this是创建对象之后产生的,静态方法优先于对象产生。
*/
public static synchronized void payTicketStatic(){
//synchronized (RunnableImpl03.class){
//先判断票是否存在
if (ticket>0){
//提高安全隐患出现的概率
try {Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName()
+"正在卖第"+ticket+"张票");
ticket--;
}
//}
}
}
(5)使用Lock锁
/**
* 实现卖票案例:出现线程安全问题(卖出了不存在的票和重复的票)
* 第三种方案:使用Lock锁
* java.util.concurrent.locks.Lock接口
* Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。
* Lock接口的方法:
* void lock()获取锁
* void unlock()释放锁
* 实现类:
* java.util.concurrent.locks.ReentrantLock
* 使用步骤:
* 1.在成员位置创建一个ReentrantLock对象
* 2.在可能会出现安全问题的代码
* (1)前调用Lock接口中的方法lock获取锁
* (2)后调用Lock接口中的方法unlock释放锁
*/
public class RunnableImpl04 implements Runnable{
//定义100张票
private static int ticket = 100;
//在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
/* //设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票重复
while (ticket > 0) {
//用Lock接口中的方法lock获取锁
l.lock();
//先判断票是否存在
if (ticket > 0) {
//提高安全隐患出现的概率
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName()
+ "正在卖第" + ticket + "张票");
ticket--;
}
//用Lock接口中的方法unlock释放锁
l.unlock();
}
}*/
//设置线程任务:卖票(将unlock放到finally)
@Override
public void run() {
//使用死循环,让卖票重复
while (ticket > 0) {
//用Lock接口中的方法lock获取锁
l.lock();
//先判断票是否存在
if (ticket > 0) {
//提高安全隐患出现的概率
try {
Thread.sleep(10);
//票存在,卖票
System.out.println(Thread.currentThread().getName()
+ "正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//用Lock接口中的方法unlock释放锁
l.unlock();//无论程序是否异常都会释放锁对象
}
}
}
}
}
5、等待与唤醒
(1)进程状态图
(2)Demo01WaitAndNotify (once做吃)
void notify()
:唤醒在此对象监视器上等待的单个线程。
等待唤醒案例:线程之间的通信
创建一个顾客线程(消费者):
告知老板要的包子的种类和数量,调用wait,放弃CPU的执行,进入waiting状态(无限等待)
创建一个老板线程(生产者):
花了5秒做包子,调用notify方法,唤醒顾客吃包子
注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait和notify方法
Object类中的方法
void wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void notify() :唤醒在此对象监视器上等待的单个线程。(会继续执行wait方法之后的代码。)
public class Demo01WaitAndNotify {
public static void main(String[] args) {
//创建一个锁对象,保证唯一
Object obj = new Object();
//创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
//使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
synchronized (obj){
System.out.println("顾客:告知老板包子数量和种类");
//调用wait,放弃CPU的执行,进入waiting状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("顾客:包子已经做好了,开吃!!");
}
}
}.start();
//创建一个老板线程(生产者)
new Thread(){
@Override
public void run() {
//花了五秒钟做包子
try {
Thread.sleep(5000);//花5s做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证等待和唤醒只能有一个在执行,需要使用同步技术
synchronized (obj){
System.out.println("老板:包子做好了,可以吃包子了。");
//做好包子之后,调用notify方法,唤醒顾客吃包子
obj.notify();
}
}
}.start();
}
}
(3)唤醒单个Demo01WaitAndNotifyWhileTrue (while做吃)
void notify()
:唤醒在此对象监视器上等待的单个线程。
public class Demo01WaitAndNotifyWhileTrue {
public static void main(String[] args) {
//创建一个锁对象,保证唯一
Object obj = new Object();
//创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
//重复买包子
while(true){
//使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
synchronized (obj){
System.out.println("顾客:告知老板包子数量和种类");
//调用wait,放弃CPU的执行,进入waiting状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("顾客:包子已经做好了,开吃!!");
System.out.println();
}
}
}
}.start();
//创建一个老板线程(生产者)
new Thread(){
//花了5秒做包子
@Override
public void run() {
//重复卖包子
while(true){
//花了五秒钟做包子
try {
Thread.sleep(5000);//花5s做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证等待和唤醒只能有一个在执行,需要使用同步技术
synchronized (obj){
System.out.println("老板:包子做好了,可以吃包子了。");
//做好包子之后,调用notify方法,唤醒顾客吃包子
obj.notify();
}
}
}
}.start();
}
}
(4)唤醒全部Demo02WaitArgsAndNotifyAll (while做吃)
void notifyAll()
:唤醒在此对象监视器上等待的所有线程。
/**
* 进入到TimeWaiting(计时等待)有两种方式
* 1.使用sleep(long m)方法
* 在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
* 2.进入到wait(long m)方法 (自动唤醒)
* wait方法如果在毫秒值结束之后,还没有被notify唤醒,
* 就会自动醒来,线程睡醒进入到Runnable/Blocked状态
*
* 唤醒的方法:
* void notify():唤醒在此对象监视器上等待的单个线程。
* void notifyAll():唤醒在此对象监视器上等待的所有线程。
*
*/
public class Demo02WaitArgsAndNotifyAll {
public static void main(String[] args) {
//创建一个锁对象,保证唯一
Object obj = new Object();
//顾客1:创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
//重复买包子
while(true){
//使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
synchronized (obj){
System.out.println("顾客1:告知老板包子数量和种类");
//调用wait,放弃CPU的执行,进入waiting状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("顾客1:包子已经做好了,开吃!!");
System.out.println();
}
}
}
}.start();
//顾客2:创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
//重复买包子
while(true){
//使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
synchronized (obj){
System.out.println("顾客2:告知老板包子数量和种类");
//调用wait,放弃CPU的执行,进入waiting状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("顾客2:包子已经做好了,开吃!!");
System.out.println();
}
}
}
}.start();
//创建一个老板线程(生产者)
new Thread(){
//花了5秒做包子
@Override
public void run() {
//重复卖包子
while(true){
//花了五秒钟做包子
try {
Thread.sleep(5000);//花5s做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证等待和唤醒只能有一个在执行,需要使用同步技术
synchronized (obj){
System.out.println("老板:包子做好了,可以吃包子了。");
//做好包子之后,调用notify方法,唤醒顾客吃包子
obj.notify();//若有多个线程,随机唤醒一个
obj.notifyAll();//唤醒所有等待的线程
}
}
}
}.start();
}
}
(5)案例:做不同种类的包子
/*
资源类:包子类
设置包子的属性
皮
陷
包子的状态: 有 true,没有 false
*/
public class BaoZi {
//皮
String pi;
//陷
String xian;
//包子的状态: 有 true,没有 false,设置初始值为false没有包子
boolean flag = false;
}
/*
生产者(包子铺)类:是一个线程类,可以继承Thread
设置线程任务(run):生产包子
对包子的状态进行判断
true:有包子
包子铺调用wait方法进入等待状态
false:没有包子
包子铺生产包子
增加一些趣味性:交替生产两种包子
有两种状态(i%2==0)
包子铺生产好了包子
修改包子的状态为true有
唤醒吃货线程,让吃货线程吃包子
注意:
包子铺线程和包子线程关系-->通信(互斥)
必须同时同步技术保证两个线程只能有一个在执行
锁对象必须保证唯一,可以使用包子对象作为锁对象
包子铺类和吃货的类就需要把包子对象作为参数传递进来
1.需要在成员位置创建一个包子变量
2.使用带参数构造方法,为这个包子变量赋值
*/
public class BaoZiPu extends Thread{
//1.需要在成员位置创建一个包子变量
private BaoZi bz;
//2.使用带参数构造方法,为这个包子变量赋值
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}
//设置线程任务(run):生产包子
@Override
public void run() {
//定义一个变量
int count = 0;
//让包子铺一直生产包子
while(true){
//必须同时同步技术保证两个线程只能有一个在执行
synchronized (bz){
//对包子的状态进行判断
if(bz.flag==true){
//包子铺调用wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行,包子铺生产包子
//增加一些趣味性:交替生产两种包子
if(count%2==0){
//生产 薄皮三鲜馅包子
bz.pi = "薄皮";
bz.xian = "三鲜馅";
}else{
//生产 冰皮 牛肉大葱陷
bz.pi = "冰皮";
bz.xian = "牛肉大葱陷";
}
count++;
System.out.println("包子铺正在生产:"+bz.pi+bz.xian+"包子");
//生产包子需要3秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//包子铺生产好了包子
//修改包子的状态为true有
bz.flag = true;
//唤醒吃货线程,让吃货线程吃包子
bz.notify();
System.out.println("包子铺已经生产好了:"+bz.pi+bz.xian+"包子,吃货可以开始吃了");
}
}
}
}
/*
消费者(吃货)类:是一个线程类,可以继承Thread
设置线程任务(run):吃包子
对包子的状态进行判断
false:没有包子
吃货调用wait方法进入等待状态
true:有包子
吃货吃包子
吃货吃完包子
修改包子的状态为false没有
吃货唤醒包子铺线程,生产包子
*/
public class ChiHuo extends Thread{
//1.需要在成员位置创建一个包子变量
private BaoZi bz;
//2.使用带参数构造方法,为这个包子变量赋值
public ChiHuo(BaoZi bz) {
this.bz = bz;
}
//设置线程任务(run):吃包子
@Override
public void run() {
//使用死循环,让吃货一直吃包子
while (true){
//必须同时同步技术保证两个线程只能有一个在执行
synchronized (bz){
//对包子的状态进行判断
if(bz.flag==false){
//吃货调用wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行的代码,吃包子
System.out.println("吃货正在吃:"+bz.pi+bz.xian+"的包子");
//吃货吃完包子
//修改包子的状态为false没有
bz.flag = false;
//吃货唤醒包子铺线程,生产包子
bz.notify();
System.out.println("吃货已经把:"+bz.pi+bz.xian+"的包子吃完了,包子铺开始生产包子");
System.out.println("----------------------------------------------------");
}
}
}
}
/*
测试类:
包含main方法,程序执行的入口,启动程序
创建包子对象;
创建包子铺线程,开启,生产包子;
创建吃货线程,开启,吃包子;
*/
public class Demo {
public static void main(String[] args) {
//创建包子对象;
BaoZi bz =new BaoZi();
//创建包子铺线程,开启,生产包子;
new BaoZiPu(bz).start();
//创建吃货线程,开启,吃包子;
new ChiHuo(bz).start();
}
}
6、线程池(POOL)
线程池:
JDK1.5之后提供的
1、 java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executors有一个静态方法:
static ExecutorService newFixedThreadPool(int nThreads)
创建一个可重用固定线程数的线程池。
参数:int nThreads:创建线程池包含的线程数量
返回值:
ExecutorService接口,返回的是ExecutorService接口中实现类对象,
我们可以使用ExecutorService接口接收(面向接口编程)
2、 java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务。
submit(Runnable task)提交一个 Runnable 任务用于执行。
关闭/销毁线程池的方法 void shutdown()
3、线程池的使用步骤:
1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4.调用ExecutorService中的方法shutdown销毁线程(不建议执行)
public class Demo01ThreadPool {
public static void main(String[] args) {
//1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(3);
//3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程
es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程
//线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程
//4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
es.shutdown();
/*
抛异常 java.util.concurrent.RejectedExecutionException
线程池都没有了,就不能获取线程了
*/
es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程
}
}
/**
* 2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
*/
public class RunnableImpl implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程!");
}
}
三、多线程相关的面试题
1、线程和进程的区别?
进程:
具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
线程:
是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
特点:
-
线程的划分尺度小于进程,这使多线程程序拥有高并发性,
-
进程在运行时各自内存单元相互独立,线程之间内存共享,
-
这使多线程编程可以拥有更好的性能和用户体验
注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。
2、一个Java应用程序至少有几个线程?
两个:
-
主线程:负责main方法代码的执行,
-
垃圾回收器线程:负责了回收垃圾。
3、如何停止一个线程?
Thread.stop()
,不建议使用
通过一个变量去控制,当符合这个条件时,自动结束。
interrupt()
4、sleep() 和 wait() 有什么区别?
sleep()方法:
-
Thread类中的静态方法,
-
当一个线程调用sleep()方法以后,不会释放同步资源锁,其他线程仍然会等待资源锁的释放。
wait()方法:
-
Object类提供的一个普通方法,
-
而且必须同同步资源锁对象在同步代码块或者同步方法中调用。
当调用wait()方法后,当前线程会立刻释放掉同步锁资源。其他线程就有机会获得同步资源锁从而继续往下执行。
5、多线程的创建方式?
方式一:继承 Thread 类
Thread 本质上也是实现了 Runnable 接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。这种方式实现多线程很简单,通过自己的类直接 extend Thread,并重写 run()方法,就可以启动新线程并执行自己定义的 run()方法。例如:继承 Thread 类实现多线程,并在合适的地方启动线程。
1 public class MyThread extends Thread {
2 public void run() {
3 System.out.println("MyThread.run()");
4 }
5
6 MyThread myThread1 = new MyThread();
7 MyThread myThread2 = new MyThread();
8 myThread1.start();
9 myThread2.start();
方式二:实现 Runnable 接口的方式
实现多线程,并且实例化 Thread,传入自己的 Thread 实例,调用 run( )方法
1 public class MyThread implements Runnable {
2 public void run() {
3 System.out.println("MyThread.run()");
4 }
5 }
6 MyThread myThread = new MyThread();
7 Thread thread = new Thread(myThread);
8 thread.start();
方式三:通过Callable和Future创建线程
1 class T implements Callable<String> {
2 @Override
3 public String call() throws Exception {
4 return null;
5 }
6 }
6、谈谈你对线程池的理解?jdk提供了哪几种线程池?他们有什么区别?
线程池可以提高线程的创建和销毁的开销
jdk提供了以下几种线程池:
-
new SingleThreadExecutor
(单线程的线程池)
只有一个线程在执行,相对于单线程执行任务 -
new FixedThreadPool
(固定线程数的线程池)
固定线程数处理任务;当任务过多,则固定的线程数谁先执行完任务,就执行剩余任务 -
new ScheduledThreadPool
(控制线程池定时周期任务执行) -
new CachedThreadPool
(可缓存的线程池)
一般工作中使用的是
new ThreadPoolExecutor
7、说一下ThreadPoolExecutor各个参数的含义?
1 ThreadPoolExecutor(
2 int corePoolSize, //核心线程池大小
3 int maximumPoolSize, //最大线程池大小
4 long keepAliveTime, //线程最大空闲时间
5 TimeUnit unit, //时间单位
6 BlockingQueue<Runnable> workQueue, //线程等待队列
7 ThreadFactory threadFactory, //线程创建工厂
8 RejectedExecutionHandler handler //拒绝策略
9 ) {
8、说一下线程的生命周期?
新建状态(New):
当线程对象对创建后,即进入了新建状态,如:Thread thread= new MyThread();
就绪状态(Runnable):
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
阻塞状态(Blocked):
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,
阻塞状态又可以分为三种:
- ①等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- ②同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- ③其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
9、启动一个线程是调用 run() 方法还是 start() 方法?
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。
run()方法是线程启动后要进行回调(callback)的方法。
10、什么情况下导致线程死锁,遇到线程死锁该怎么解决?
死锁的定义:
死锁是指多个线程因竞争资源而造成的一种互相等待状态,若无外力作用,这些进程都将无法向前推进。
死锁的条件:
互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程
所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1)占有(i=0, 1, …, n-1),Pn 等待的资源被 P0 占有,如图所示:
避免死锁:
①加锁顺序(线程按照一定的顺序加锁)
②加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
11、什么是乐观锁和悲观锁?
悲观锁
Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。
乐观锁
乐观锁(Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
12、乐观锁一定就是好的吗?
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
- 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,
但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。 - 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
- ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。
解决的思路是引入版本号,每次变量更新都把版本号加一。
13、说一下线程池的启动策略?
线程池的执行过程描述:
1/*
2* Proceed in 3 steps:
3*
4* 1. If fewer than corePoolSize threads are running, try to
5* start a new thread with the given command as its first
6* task. The call to addWorker atomically checks runState and
7* workerCount, and so prevents false alarms that would add
8* threads when it shouldn't, by returning false.
9*
10* 2. If a task can be successfully queued, then we still need
11* to double-check whether we should have added a thread
12* (because existing ones died since last checking) or that
13* the pool shut down since entry into this method. So we
14* recheck state and if necessary roll back the enqueuing if
15* stopped, or start a new thread if there are none.
16*
17* 3. If we cannot queue task, then we try to add a new
18* thread. If it fails, we know we are shut down or saturated
19* and so reject the task.
20*/
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute
() 方法添加一个任务时,线程池会做如下判断:
-
①如果正在运行的线程数量小于
corePoolSize
,那么马上创建线程运行这个任务; -
②如果正在运行的线程数量大于或等于
corePoolSize
,那么将这个任务放入队列。 -
③如果这时候队列满了,而且正在运行的线程数量小于
maximumPoolSize
,那么还是要创建线程运行这
个任务; -
④如果队列满了,而且正在运行的线程数量大于或等于
maximumPoolSize
,那么线程池会抛出异常,告
诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime
)时,线程池会判断,如果当前运行的线程数大于corePoolSize
,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize
的大小。
14、请说出同步线程及线程调度相关的方法?
wait():
使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():
使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
notify():
唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notityAll():
唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
注意: java 5 通过 Lock 接口提供了显示的锁机制,Lock 接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调
15、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
不是。线程池默认初始化后不启动 Worker,等待有请求时才启动。
当调用 execute方法添加一个任务时,线程池会做如下判断:
-
如果正在运行的线程数量小于
corePoolSize
,那么马上创建线程运行这个任务; -
如果正在运行的线程数量大于或等于
corePoolSize
,那么将这个任务放入队列; -
如果这时候队列满了,而且正在运行的线程数量小于
maximumPoolSize
,那么还是要创建非核心线程立刻运行这个任务; -
如果队列满了,而且正在运行的线程数量大于或等于
maximumPoolSize
,那么线程池会抛出异常RejectExecutionException
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAlive)时,线程池会判断。
如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize的大小。
16、Synchronized的原理是什么?
Synchronized是由JVM实现的一种实现互斥同步的方式,查看被Synchronized修饰过的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter
和monitorexit
两个字节码指令。
在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;
当执行monitorexit指令时,将锁计数器-1;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
Java中Synchronize通过在对象头设置标志,达到了获取锁和释放锁的目的。
17、为什么说Synchronized是非公平锁?
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
18、JVM对java的原生锁做了哪些优化?
在Java6之前, Monitor的实现完全依赖底层操作系统的互斥锁来实现.
由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化。
一种优化是使用自旋锁
,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
现代JDK中还提供了三种不同的 Monitor实现,也就是三种不同的锁:
偏向锁(Biased Locking)
轻量级锁
重量级锁
这三种锁使得JDK得以优化 Synchronized的运行,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。当没有竞争出现时,默认会使用偏向锁。
JVM会利用CAS操作,在对象头上的 Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。
如果有另一线程试图锁定某个被偏向过的对象,JVM就撤销偏向锁,切换到轻量级锁实现。
轻量级锁依赖CAS操作 Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁否则,进一步升级为重量级锁。
19、Synchronized和 ReentrantLock的异同?
synchronized:
是java内置的关键字,它提供了一种独占的加锁方式。
synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。然而synchronized也有一些问题:
当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。
如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。
ReentrantLock:
ReentrantLock是Lock的实现类,是一个互斥的同步锁。
ReentrantLock是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
公平锁与非公平锁多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
从功能角度:
ReentrantLock比 Synchronized的同步操作更精细
(因为可以像普通对象一样使用),甚至实现 Synchronized没有的高级功能,如:
-
等待可中断当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
-
带超时的获取锁尝试在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。
-
可以判断是否有线程在排队等待获取锁。
-
可以响应中断请求与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
-
可以实现公平锁。
从锁释放角度:
Synchronized在JVM层面上实现的
,不但可以通过一些监控工具监控 Synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定,但是使用Lock则不行,Lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()
放到 finally{}
中。
从性能角度:
Synchronized早期实现比较低效
,对比 ReentrantLock,大多数场景性能都相差较大。
但是在Java6中对其进行了非常多的改进:
- 在竞争不激烈时:Synchronized的性能要优于 ReetrantLock;
- 在高竞争情况下:Synchronized的性能会下降几十倍,但是 ReetrantLock的性能能维持常态。
20、volatile关键字的作用?
对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。主要的原理是使用了内存指令。
LoadLoad重排序:一个处理器先执行一个L1读操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再L1
StoreStore重排序:一个处理器先执行一个W1写操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再W1
LoadStore重排序:一个处理器先执行一个L1读操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再L1
StoreLoad重排序:一个处理器先执行一个W1写操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再W1
21、说一下volatile关键字对原子性、可见性以及有序性的保证?
在volatile变量写操作
的前面会加入一个Release
屏障,然后在之后会加入一个Store
屏障,这样就可以保证volatile写跟Release屏障之前的任何读写操作都不会指令重排,然后Store屏障保证了,写完数据之后,立马会执行flush处理器缓存的操作。
在volatile变量读操作
的前面会加入一个Load
屏障,这样就可以保证对这个变量的读取时,如果被别的处理器修改过了,必须得从其他 处理器的高速缓存(或者主内存)中加载到自己本地高速缓存里,保证读到的是最新数据;在之后会加入一个Acquire
屏障,禁止volatile读操作之后的任何读写操作会跟volatile读指令重排序。
与volatie读写内存屏障对比一下,是类似的意思。
Acquire屏障:其实就是LoadLoad屏障 + LoadStore屏障,
Release屏障:其实就是StoreLoad屏障 + StoreStore屏障
22、什么是CAS?
CAS(compare and swap)的缩写。Java利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的。
CAS有3个操作数:内存值V
,旧的预期值A
,要修改的新值B
。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS的缺点:
CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
ABA问题
这是CAS机制最大的问题所在。
23、什么是AQS?
AQS,即AbstractQueuedSynchronizer,队列同步器
,它是Java并发用来构建锁和其他同步组件的基础框架。
同步组件对AQS的使用:
AQS是一个抽象类,主是是以继承的方式使用。
AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。查看源码可知,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。
抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列)
1public class CountDownLatch {
2 /**
3 * Synchronization control For CountDownLatch.
4 * Uses AQS state to represent count.
5 */
6 private static final class Sync extends AbstractQueuedSynchronizer {
7 private static final long serialVersionUID = 4982264981922014374L;
8
9 Sync(int count) {
10 setState(count);
11 }
12
13 int getCount() {
14 return getState();
15 }
16
17 protected int tryAcquireShared(int acquires) {
18 return (getState() == 0) ? 1 : -1;
19 }
20
21 protected boolean tryReleaseShared(int releases) {
22 // Decrement count; signal when transition to zero
23 for (;;) {
24 int c = getState();
25 if (c == 0)
26 return false;
27 int nextc = c-1;
28 if (compareAndSetState(c, nextc))
29 return nextc == 0;
30 }
31 }
32 }
33}
24、Semaphore是什么?
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。
semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。
由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
1public static void main(String[] args) {
2 int N = 8; //工人数
3 Semaphore semaphore = new Semaphore(5); //机器数目
4 for(int i=0;i<N;i++)
5 new Worker(i,semaphore).start();
6 }
7 static class Worker extends Thread{
8 private int num;
9 private Semaphore semaphore;
10 public Worker(int num,Semaphore semaphore){
11 this.num = num;
12 this.semaphore = semaphore;
13 }
14 @Override
15 public void run() {
16 try {
17 semaphore.acquire();
18 System.out.println("工人"+this.num+"占用一个机器在生产...");
19 Thread.sleep(2000);
20 System.out.println("工人"+this.num+"释放出机器");
21 semaphore.release();
22 } catch (InterruptedException e) {
23 e.printStackTrace();
24 }
25 }
26 }