1 线程的特点及进程和线程的区别:
- 轻量级进程
- 独立调度的基本单位
- 可并发执行
- 共享进程资源
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 | 线程可以看成轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小 |
所处环境 | 再操作系统中能同时运行多个任务(程序) | 再同一应用中有多个顺序流同时执行 |
分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域 | 除了CPU之外,不会为线程分配内(线程所使用的资源是它所属的进程的资源),线程组只能共享资源 |
包含关系 | 没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。 | 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。 |
2 两种线程创建方式的比较
- 方式一:继承Thread类方式的多线程
优势:编写简单
劣势:无法继承其它父类(因为java是单继承) - 方式二:实现Runnable接口方式的多线程
优势:可以继承其它类,多线程可共享同一个Runnable对象。
劣势:编程方式稍微复杂,如果需要访问当前线程,需要调用Thread.currentThread()方法
实现Runnable接口方式要通用一些。
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
点进源码可以发现Thread类其实也是实现了Runnable接口
3 Thread类常用方法
方法 | 功能 |
---|---|
static Tread currentThread() | 得到当前线程 |
getName() | 返回线程名称 |
setName(String name) | 设置线程名称 |
int getPriority | 获得线程的优先级数值 |
viod setPriortiry | 设置线程的优先级数值 |
void start | 调用run()方法启动线程,开始执行线程 |
void run() | 存放线程体代码 |
isAlive() | 判断线程是否还”活着“,即线程是否还未终止 |
示例1
public class JoinThread extends Thread{
public JoinThread() {
super();
}
public void run() {
for (int i = 1; i <=10; i++) {
System.out.println(i+this.getName()+"=============");
}
}
public static void main(String[] args) {//main方法是一个单独的线程
for (int i = 1; i <=20; i++) {
System.out.println(i+"-------csdn------");
if (i == 5) {
JoinThread joinThread = new JoinThread();
joinThread.start();
try {
joinThread.join();//谁调用join方法,谁就强占cpu资源,直至执行结束
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
示例2
class TortoiseRunnable implements Runnable {
@Override
public void run() {
//Thread.currentThread().setName("乌龟线程");
while(true){
System.out.println("乌龟领先了-----"+Thread.currentThread().getName());
}
}
}
public class TestThreadTwo {
public static void main(String[] args) {
Thread.currentThread().setName("兔子线程");
TortoiseRunnable tr = new TortoiseRunnable();
Thread th = new Thread(tr);
th.setName("乌龟线程");
th.start();
// Thread th2 = new Thread(tr);
// Thread th3 = new Thread(tr);
while(true){
System.out.println("兔子领先了-----"+Thread.currentThread().getName());
}
}
}
线程的生命周期图解
4 线程的控制方法
方法 | 作用 | 补充 |
---|---|---|
join () -强势 | 阻塞指定线程等到另一个线程完成以后再继续执行 | 调用Jion方法的线程,完全获取cpu的使用权,直到这个线程结束为止,其他线程处于阻塞等待状态。 |
sleep () -睡眠 | 使线程停止运行一段时间,将处于阻塞状态 | 如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行! |
yield () -投降 | 让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态 | 如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行! |
setDaemon(true)- 可以将指定的线程设置成后台线程(也叫守护线程) | 创建后台线程的线程结束时,后台线程也随之消亡 | 只能在线程启动之前把它设为后台线程 |
stop() 停止 | 结束线程 | (不推荐使用) |
示例1:优先级别
/**
* 查看和修改线程的优先级别
*
* 1.如何查看优先级别
* getPriority()
*
* 2.如何修改优先级别
* setPriority(7);
*
* 3.优先级别的级别
* public final static int MIN_PRIORITY = 1;
* public final static int MAX_PRIORITY = 10;
* public final static int NORM_PRIORITY = 5;
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程。
*/
class TortoiseThread extends Thread{
/**
* 线程体
*/
@Override
public void run() {
//this.setName("乌龟线程");
while(true){
System.out.println("乌龟领先了-----"+this.getName()+",优先级别:"+this.getPriority());
}
}
}
public class TestThread1 extends Object{
public static void main(String[] args) {
Thread.currentThread().setName("兔子线程");
Thread.currentThread().setPriority(8);
TortoiseThread tt = new TortoiseThread();//新生状态
//tt.run();
tt.setName("乌龟线程");
tt.setPriority(7);
tt.start();//启动线程 就绪状态
while(true){
System.out.println("兔子领先了----"+Thread.currentThread().getName()
+",优先级别:"+Thread.currentThread().getPriority());
}
}
}
示例2:join
/**
* join 阻塞指定线程等到另一个线程完成以后再继续执行
* 注意事项:join应该在start之后
*/
public class TestJoin extends Thread{
public TestJoin() {
super();
}
public TestJoin(String name) {
super(name);
}
public void run(){
for(int i=0;i<3;i++){
System.out.println(i+"---"+this.getName()+"----");
}
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(i+"----main-----------");
if(i==5){
TestJoin tj = new TestJoin("程咬金");
tj.start();
try {
tj.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果
0----main-----------
1----main-----------
2----main-----------
3----main-----------
4----main-----------
5----main-----------
0---程咬金----
1---程咬金----
2---程咬金----
6----main-----------
7----main-----------
8----main-----------
9----main-----------
示例3:Sleep
/**
* 功能1:预备 3 2 1 开始
*/
public class TestSleep {
public static void main(String[] args) {
System.out.println("预备");
for(int i=3;i>0;i--){
System.out.println(i);
//sleep 1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("开始");
}
}
运行结果
预备
3(输出后睡眠一秒)
2(输出后睡眠一秒)
1(输出后睡眠一秒)
开始
示例4:yield() 投降
/**
* Thread.yield()
* 让出CPU,进入就绪状态,继续排队抢占CPU
* 如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!
下面程序达到一种交替输出的效果,但是不明显!有点随机性
*/
public class TestYield {
public static void main(String[] args) {
//创建两个线程
FirstThread ft = new FirstThread();
SecondThread st = new SecondThread();
//启动两个线程
ft.start();
st.start();
}
}
class FirstThread extends Thread{
public void run(){
for(int i=1;i<=10;i++){
System.out.println("firstThread "+i);
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Thread.yield();
}
}
}
class SecondThread extends Thread{
public void run(){
for(int i=1;i<=10;i++){
System.out.println("secondThread "+i);
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Thread.yield();
}
}
}
Thread.sleep(10)后的运行结果
firstThread 1
secondThread 1
firstThread 2
secondThread 2
firstThread 3
secondThread 3
firstThread 4
secondThread 4
firstThread 5
secondThread 5
secondThread 6
firstThread 6
secondThread 7
firstThread 7
firstThread 8
secondThread 8
secondThread 9
firstThread 9
firstThread 10
secondThread 10
示例5:Daemon-守护线程
- 线程对象. setDaemon (true) ;设置为守护线程
- 线程有两类:用户线程(前台线程)、守护线程(后台线程)
- 如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
- 垃圾回收器线程属于守护线程。
/**
* Daemon后台程序
* 功能:如果main线程结束后,由main线程启动的线程也应该随之结束。
* 解决:定义为后台线程
* setDaemon(true);
*/
public class TestSetDaemon extends Thread{
public void run(){
while(true){
System.out.println("-----"+this.getName()+"------");
}
}
public static void main(String[] args) {
TestSetDaemon tsd = new TestSetDaemon();
tsd.setDaemon(true);//必须放在start之前
tsd.start();
for(int i=0;i<100;i++){
System.out.println("-------main-----------"+i);
}
}
}
5 线程同步
同步代码块 | synchronized (obj){ } |
---|---|
同步方法 | private synchronized void makeWithdrawal(int amt) {} |
同步监视器
- synchronized (obj){ }中的obj称为同步监视器
- 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器。
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,也就是该对象本身。
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问,等待
- 第一个线程访问完毕,解锁同步监视器。
- 第二个线程访问,发现同步监视器未锁,锁定并访问
线程同步的好处 | 解决了线程安全问题 |
---|---|
线程同步的缺点 | –性能下降(会带来死锁) |
死锁(多线程编程时应该注意避免死锁的发生 | 当两个线程相互等待对方释放“锁”时就会发生死锁,出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。 |
同步示例1:四个窗口卖票不安全问题
功能3:
public class TicketRunnable implements Runnable {
private int tickNum=200;
@Override
public void run() {
while(tickNum>0){
//1
System.out.println(Thread.currentThread().getName()+"窗口卖出第"+tickNum+"张票");
//----切换
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2
tickNum--;
}
}
}
------------------------------------------------------------------------------
/**
* 经典应用:模拟线程的切换,让线程在此处交替执行
*/
public class TestTicket{
public static void main(String[] args) {
//创建四个窗口线程
TicketRunnable tr = new TicketRunnable();
Thread th1 = new Thread(tr);
Thread th2 = new Thread(tr);
Thread th3= new Thread(tr);
Thread th4 = new Thread(tr);
//开始卖票
th1.start();
th2.start();
th3.start();
th4.start();
}
}
运行结果(同一张票会卖出多次)
Thread-0窗口卖出第44张票
Thread-1窗口卖出第44张票
Thread-2窗口卖出第44张票
Thread-2窗口卖出第41张票
Thread-0窗口卖出第40张票
Thread-1窗口卖出第41张票
同步示例2:使用线程同步解决四个窗口卖票安全问题
public class TestTicket {
public static void main(String[] args) {
//1.
TicketRunnable tr = new TicketRunnable();
//TicketRunnable2 tr = new TicketRunnable2();
Thread th1 = new Thread(tr);//1、新生状态
Thread th2 = new Thread(tr);
Thread th3 = new Thread(tr);
Thread th4 = new Thread(tr);
th1.start();//2、就绪状态
th2.start();
th3.start();
th4.start();
}
}
/**
解决:方式一 同步代码块
用Runnable接口 共享票数
最终解决:线程同步(后面解决)
synchronized
sleep的作用
* 阻塞当前的线程,指定的时间后解除阻塞
* 在此时间内,即使cpu是空闲的,线程亦不会解除阻塞
* @author enbo
* @date 2018年3月1日
*/
class TicketRunnable implements Runnable{
private Integer num = 50;
@Override
public void run() {//3.运行状态
while(num > 0){//循环
synchronized (this) {
if(num <=0){
break;
}
System.out.println(Thread.currentThread().getName()+" 卖了第"+(num)+"张票!");
// try {
// Thread.sleep(10);//放弃cpu的执行权 睡眠状态 挂起
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//切换
num--;//
}
}
}
}
/**
解决:方式二:同步方法
用Runnable接口 共享票数
最终解决:线程同步(后面解决)
synchronized
sleep的作用
* 阻塞当前的线程,指定的时间后解除阻塞
* 在此时间内,即使cpu是空闲的,线程亦不会解除阻塞
* @author enbo
* @date 2018年3月1日
*/
class TicketRunnable2 implements Runnable{
private int num = 50;
@Override
public void run() {//3.运行状态
while(num > 0){//
if(num <= 0){
break;
}else{
sellTickt();
}
}
}
private synchronized void sellTickt() {
if(num > 0){
System.out.println(Thread.currentThread().getName()+" 卖了第"+(num)+"张票!");
try {
Thread.sleep(10);//放弃cpu的执行权 睡眠状态 挂起
} catch (InterruptedException e) {
e.printStackTrace();
}
//切换
num--;//
}
}
}
同步示例3:使用同步方法(TicketRunnable2)解决四个窗口卖票安全问题(写法2)
class TicketRunnable2 implements Runnable{
private int num = 50;
public void run(){
while(true){
boolean flag = sellOne();
if(flag == true){
break;
}
}
}
private synchronized boolean sellOne() {
if(num<=0){
return true;
} else {
System.out.println(Thread.currentThread().getName()+" 卖了第"+(num)+"张票!");
try {
Thread.sleep(10);//放弃cpu的执行权 睡眠状态 挂起
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;//
return false;
}
}
}
死锁
/**
* 模拟两个哲学家吃饭的问题
* 创建连个锁对象
*/
class MyLock {
//两个锁(两根筷子)
public static Object a = new Object();
public static Object b = new Object();
}
class Boy extends Thread{
@Override
public void run() {
synchronized (MyLock.a) {
System.out.println("男孩拿到了a筷子");
synchronized(MyLock.b){
System.out.println("男孩拿到了b筷子");
System.out.println("男孩可以吃东西啦!");
}
}
}
}
class Girl extends Thread{
@Override
public void run() {
synchronized (MyLock.b) {
System.out.println("女孩拿到了b筷子");
synchronized(MyLock.a){
System.out.println("女孩拿到了a筷子");
System.out.println("女孩可以吃东西啦!");
}
}
}
}
public class Test {
public static void main(String[] args) {
Boy boy = new Boy();
Girl girl = new Girl();
boy.start();
girl.start();
}
}
/**
* 增加睡眠时间解决死锁问题
*/
//public class Test {
// public static void main(String[] args) throws Exception {
// Boy boy = new Boy();
// Girl girl = new Girl();
// boy.start();
// Thread.sleep(100);//增加睡眠,解除死锁问题
// girl.start();
// }
//}
6 线程通信
•应用场景:生产者和消费者问题
–假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
–如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
–如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
•分析
•这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
•对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费
•对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费
•
•在生产者消费者问题中,仅有synchronized是不够的
•synchronized可阻止并发更新同一个共享资源,实现了同步
•synchronized不能用来实现不同线程之间的消息传递(通信)
方法名 | 作用 |
---|---|
final void wait() | 表示线程一直等待,直到其他线程通知 |
void wait(long timeout) | 线程等待指定毫秒参数时间 |
fianl void wait(long timeout,int nanos) | 线程等待指定毫秒,微秒的时间 |
final void notify() | 唤醒一个处于等待状态的线程 |
final coid notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先执行 |
注意:均是java.lang.Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常(java.lang.IllegalMonitorStateException) |
案例一(生产者生产一件,消费者消费一件)
生产者生产面包,消费者消费面包,要求仓库有面包时,提醒消费者消费,生产者出于等待状态;当没有面包的时候,提醒生产者生产,消费者处于等待。即生产一件,消费一件。
基本思路: |
---|
1.创建面包类,构建相关的属性和方法。 |
2.创建面包仓库类,面包类对象传入,并创建sychronized修饰的同步方法——input和output方法。 |
3.创建两个线程,生产者product和消费者consumer,分别继承Runnable,重写run()方法,并分别调input和output方法,保证同步。 |
4.在主方法创建Thread,并start。 |
/**
* 面包类实体类
*/
class Bread {
private int id;
private String productName;
public Bread() {
}
public Bread(int id, String productName) {
this.id = id;
this.productName = productName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
}
/**
* 面包容器实体类
*/
class BreadCon {
private Bread con;
private boolean flag = false; //判断仓库是否有面包,false表示没有
public synchronized void input(Bread b) throws InterruptedException {
if (flag) {
this.wait(); //如果仓库有面包,则处于阻塞状态,等待消费者消费后唤醒
}
this.con = b;
System.out.println(Thread.currentThread().getName() + "生产了"
+ b.getId() + "号面包");
flag = true;
this.notify(); //仓库没有面包,则生产面包,并将标记改为true,唤醒消费者购买
}
public synchronized void output() throws InterruptedException {
if (!flag) {
this.wait();
}
Bread b= con;
con = null;
System.out.println(Thread.currentThread().getName()+"消费了"
+b.getId() + "号面包" +" 生产者:"+b.getProductName());
flag = false; //修改标记
this.notify(); //唤醒生产者生产
}
}
/**
* 生产者类
*/
class Product implements Runnable{
private BreadCon con;
public Product(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
Bread b = new Bread(i, Thread.currentThread().getName());
try {
this.con.input(b);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者类
*/
class Consumer implements Runnable {
private BreadCon con;
public Consumer(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
try {
con.output();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 测试类
*/
public class test {
public static void main(String[] args) {
//创建容器
BreadCon con = new BreadCon();
//生产
Product product = new Product(con);
//消费
Consumer consumer = new Consumer(con);
//线程对象启动
new Thread(product, "面包生产商A").start();
new Thread(consumer, "消费者1").start();
}
}
运行结果
面包生产商A生产了1号面包
消费者1消费了1号面包 生产者:面包生产商A
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
面包生产商A生产了3号面包
消费者1消费了3号面包 生产者:面包生产商A
面包生产商A生产了4号面包
......
如果生产方不止一个,消费方也不止一个,如何设计?
我们分别为生产者和消费者创建两个线程
new Thread(product, "面包生产商A").start();
new Thread(product, "面包生产商B").start();
new Thread(consumer, "消费者1").start();
new Thread(consumer, "消费者2").start();
运行结果(可能产生死锁和错误状态)
面包生产商A生产了1号面包
Exception in thread "消费者2" 消费者1消费了1号面包 生产者:面包生产商A
面包生产商B生产了1号面包
面包生产商A生产了2号面包
消费者1消费了2号面包 生产者:面包生产商A
面包生产商B生产了2号面包
面包生产商A生产了3号面包
消费者1消费了3号面包 生产者:面包生产商A
面包生产商B生产了3号面包
java.lang.NullPointerException
面包生产商A生产了4号面包
at com.csqf.thread._a.BreadCon.output(test.java:56)
at com.csqf.thread._a.Consumer.run(test.java:95)
at java.lang.Thread.run(Thread.java:745)
消费者1消费了4号面包 生产者:面包生产商A
解决:案例二(生产方不止一个,消费方也不止)
将 if 判断语句改为 while 循环判断语句,保证被唤醒后,不再继续执行生产面包代码,而是返回进行循环判断判断
将 notify 改为 notifyAll ,避免发生死锁情况。
//这里只针对面包容器类进行修改
public class BreadCon {
private Bread con;
private boolean flag = false; //判断仓库是否有面包,false表示没有
public synchronized void input(Bread b) throws InterruptedException {
while (flag) {
this.wait();
}
this.con = b;
System.out.println(Thread.currentThread().getName() + "生产了" + b.getId()
+ "号面包");
flag = true;
this.notifyAll();
}
public synchronized void output() throws InterruptedException {
while (!flag) {
this.wait();
}
/*Bread b= con;
con = null;*/
System.out.println(Thread.currentThread().getName()+"消费了"+con.getId()
+ "号面包" +" 生产者:"+con.getProductName());
flag = false;
this.notifyAll();
}
}
虽然解决了错误和死锁问题,但如果生产者和消费者数量及其庞大,每次notifyAll会将无关的线程也唤醒,增加了无效的判断时间,效率大打折扣,所以这里采用Condition接口,配合Lock接口下的ReentrantLock实现类解决效率问题。
采用Condition接口,配合Lock接口下的ReentrantLock实现类解决效率问题。
sychronized的锁方式,只能让生产者和消费者进入同一个阻塞队列,所以notifyAll会将队列中所有线程唤醒。
Condition接口则提供了两个队列,一个消费队列,一个生产队列,进行锁的微操,甚至可以自定义提供三个甚至更多队列,实现多线程的同步,并保证效率。
方法的使用
//创建Lock对象
Lock lock = new ReentrantLock();
//根据需求创建自定义数量的Condition条件队列
Condition condition = lock.newCondition();
//使某一队列等待,相当于wait()
condition.await();
//使某一队列唤醒,相当于notify()
condition.signal();
代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 面包类实体类
*/
class Bread {
private int id;
private String productName;
public Bread() {
}
public Bread(int id, String productName) {
this.id = id;
this.productName = productName;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
}
/**
* 面包容器实体类
*/
//这里只提供核心代码块
class BreadCon {
/*这里必须用-1,否则出现索引越界异常,因为后面消费者消费index--会出现index=-1的情况
*为了避免因为生产者生产的时候breads[index]调取异常
*会将index++放到赋值之前,保证索引从0开始
*/
private int index = -1;
private Bread[] breads = new Bread[5];
//创建ReentrantLock锁对象,并创建两个阻塞队列,一个是生产者proCon,一个是消费者conCon
private Lock lock = new ReentrantLock();
private Condition proCon = lock.newCondition();
private Condition conCon = lock.newCondition();
public BreadCon() {
}
public void input(Bread bread) {
lock.lock(); //上锁
try {
while (index > 3) {
proCon.await(); //如果面包数量超过数组容量,则生产者处于生产阻塞队列
}
index++; //因为Index是从-1开始,所以放到赋值之前,从0开始索引
breads[index] = bread;
System.out.println("生产者:" + Thread.currentThread().getName()
+ "生产了" + bread.getId() + "号面包");
conCon.signal(); //如果有面包,则将消费队列消费者唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}
public void output() {
lock.lock();
try {
while (index < 0) {
try {
conCon.await(); //如果没有面包,则将消费者置于消费阻塞队列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Bread b = breads[index];
breads[index] = null;
System.out.println("消费者:" + Thread.currentThread().getName()
+ "消费了" + b.getId() + "号面包,生产者是:" + b.getProductName());
index--;
proCon.signal(); //每消费一次面包,可以唤醒生产者生产
} finally {
lock.unlock();
}
}
}
/**
* 生产者类
*/
class Product implements Runnable{
private BreadCon con;
public Product(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
Bread b = new Bread(i, Thread.currentThread().getName());
this.con.input(b);
}
}
}
/**
* 消费者类
*/
class Consumer implements Runnable {
private BreadCon con;
public Consumer(BreadCon con) {
this.con = con;
}
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
con.output();
}
}
}
/**
* 测试类
*/
public class test {
public static void main(String[] args) {
//创建容器
BreadCon con = new BreadCon();
//生产
Product product = new Product(con);
//消费
Consumer consumer = new Consumer(con);
//线程对象启动
new Thread(product, "面包生产商A").start();
new Thread(product, "面包生产商B").start();
new Thread(consumer, "消费者1").start();
new Thread(consumer, "消费者2").start();
}
}
案例二:三个线程交替输出A B C ,输出20遍
这里需要构筑三个Condition队列锁。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class PrintABC {
private int flag = 1; //1表示A, 2表示B, 3表示C
private Lock lock = new ReentrantLock();
private Condition aCon = lock.newCondition();
private Condition bCon = lock.newCondition();
private Condition cCon = lock.newCondition();
public void printA() {
lock.lock();
try {
while (1 != flag) {
//这里注意,必须将阻塞放到循环判断中
//这样最后一次循环的时候,A被唤醒,可以继续执行下面的代码,依次唤醒BC,线程退出
//否则,如果将后面代码放到判断里,将里面的阻塞放到外面,则A被唤醒,却不能调用signal()方法,导致B/C一直被阻塞,main线程退出,但是子线程还在,发生死锁,后面循环也不会继续
aCon.await();
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName());
bCon.signal();
flag = 2;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (2 != flag) {
bCon.await();
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName());
cCon.signal();
flag = 3;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (3 != flag) {
cCon.await();
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName());
aCon.signal();
flag = 1;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//测试方法
public class test {
public static void main(String[] args) {
PrintABC printABC = new PrintABC();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
printABC.printA();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
printABC.printB();
}
}
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
printABC.printC();
}
}
}, "C").start();
}
}
7 认识线程池
线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。
频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。
线程池 |
---|
线程容器,可设定线程分配的数量上限 |
将预先创建的线程对象存入池中,并重用线程池中的线程对象 |
避免频繁的创建和销毁 |
线程池原理(图解)
原理:将任务提交给线程池,由线程池分配线程、运行任务,并在当前任务结束后复用线程。**
常用的线程池接口和类(所在包java.util.concurrent):
-
Executor:线程池的顶级接口。(查看API)
— void execute(Runnable command); -
ExecutorService:线程池接口,可通过submit(Runnable task) 提交任务代码。
-
Executors工厂类:通过此类可以获得一个线程池。
—通过 newFixedThreadPool(int nThreads) 获取固定数量的线程池。
参数:指定线程池中线程的数量。
—通过newCachedThreadPool() 获得动态数量的线程池,如不够则创建新的,无上限。
案例示例(线程池的创建)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 演示线程池的创建
* Executor:线程池的顶级接口。
--- void execute(Runnable command);
ExecutorService:线程池接口,可通过submit(Runnable task) 提交任务代码。
--- ThreadPoolExecutor
---ScheduledThreadPoolExecutor
以上两个线程池创建实现类可以创建线程,但是不推荐(一般用线程池工具类更方便、简单)
Executors:创建线程池的工具类(推荐)
(1)创建固定线程个数的线程池 newFixedThreadPool()
(2)创建缓存线程池(由任务的多少来决定)newCachedThreadPool()
(3)创建单线程池 newSingleThreadExecutor()
(4)创建调度线程池(调度:按 周期、定时 执行) newScheduledThreadPool()
* @author enbo
* @date 2020年7月26日
*/
public class Test {
public static void main(String[] args) {
//1、创建线程(两种方式)
//1.1、创建固定线程个数的线程池
ExecutorService es = Executors.newFixedThreadPool(4);
//1.2、创建缓存线程池,线程个数由任务个数决定(动态创建)
//ExecutorService es = Executors.newCachedThreadPool();
//1.3、创建单线程线程池(简单演示)
//ExecutorService es = Executors.newSingleThreadExecutor();
//1.4、创建调度线程池(调度:按 周期、定时 执行) (简单演示)
//Executors.newScheduledThreadPool(corePoolSize);
//2、创建任务
//2.1 创建线程任务:卖火车票
Runnable runnable = new Runnable(){
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+"卖了第"+ticket+"张票");
ticket--;
}
}
};
//3、提交任务
//2.2 for循环表示提交四次(因为线程池有四个线程)
for (int i = 0; i < 4; i++) {
es.submit(runnable);//提交一次,从池中获取一个线程,并启动
}
//3、关闭线程池
es.shutdown();//等待所有任务执行完毕,然后关闭线程池
//es.shutdownNow();//不会等待所有任务执行完毕,立即关闭线程池
}
}
Callable接口
Callable接口是创建线程类的第三种方式,前面两种方式分别是继承Thread类和实现Runnable接口。
JDK5加入,与Runnable接口类似,实现之后代表一个线程任务。
Callable具有泛型返回值、可以声明异常。
public interface Callable<V>{
public V call() throws Exception;
}
案例示例(演示Callable的基本使用)
注意:如果只是为了创建线程,Callable其实看上去有点繁琐,还不如直接用前两种方式还简单一些,但是Callable结合线程池使用,就会很方便了。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* 演示Callable接口的使用
*
* Callable与Runnable接口的区别?
* Callable具有泛型返回值、可以声明异常,而Runnable接口没有。
*
* 注意:Callable类似 Runnable,但是不能直接交给Thread,需要转换一个可执行任务 FutureTask
* 而 FutureTask 间接实现了 Runnable接口(查看源码)
功能:使用 Callable 实现 1-100的和
*/
public class Demo2 {
public static void main(String[] args) throws Exception {
//1、创建Callable对象
Callable<Integer> callable = new Callable<Integer>(){
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"开始了。。。");
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum+=i;
Thread.sleep(100);//故意休眠100毫秒
}
return sum;
}
};
//2、Callable类似 Runnable,但是不能直接交给Thread,需要转换一个可执行任务 FutureTask
FutureTask<Integer> task = new FutureTask<Integer>(callable);
//3、创建线程,交给Thread
Thread th = new Thread(task);
//4、启动线程
th.start();
//5、获取结果(task.get() 会等待 call()方法执行完毕,才会返回)
Integer sum = task.get();
System.out.println(sum);
}
}
运行结果
Thread-0开始了。。。
5050
Callable结合线程池使用
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* Callable结合线程池使用
* 功能:使用线程池计算1-100的和
*
* Future表示将要执行完任务的结果
*
*/
public class Demo3 {
public static void main(String[] args) throws Exception, ExecutionException {
//1、创建线程池
ExecutorService es = Executors.newFixedThreadPool(1);
//2、提交任务 Future表示将要执行完任务的结果
Future<Integer> future = es.submit(new Callable<Integer>(){
//Callable接口是创建线程类的第三种方式,前面两种方式分别是继承Thread类和实现//Runnable接口。
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"开始执行了。。。");
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum+=i;
Thread.sleep(10);//故意休眠10ms
}
return sum;
}
});
//3、获取结果,等待任务执行完毕才返回
System.out.println(future.get());
//4、关闭线程池
es.shutdown();
}
}
注意:实际工作中,我们一般也是使用 Callable与线程池联合使用!
Future接口(示例案例) : 表示将要完成任务的结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 需求:使用两个线程,并发计算1~50、51~100的和,再进行汇总统计。
*/
public class Demo4 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//1、创建线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//2、提交任务
//第1个任务
Future<Integer> future1 = es.submit(new Callable<Integer>(){
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 50; i++) {
sum+=i;
}
System.out.println("1-50计算完毕!");
return sum;
}
});
//第2个任务
Future<Integer> future2 = es.submit(new Callable<Integer>(){
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 51; i <= 100; i++) {
sum+=i;
}
System.out.println("51-100计算完毕!");
return sum;
}
});
//3、获取结果
int sum = future1.get()+future2.get();
System.out.println("结果是:"+sum);
//4、关闭线程池
es.shutdown();
}
}
同步(图解)
异步(图解)
8 (面试题)thread.Join
(有三个线程t1、t2、t3。确保三个线程t1执行完后t2执行,t2执行完成后t3执行)
/**
*把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的*Join()方法,直到线程A执行完毕后,才会继续执行线程B。
*/
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new Thread(new Work(null),"线程t1");
Thread t2 = new Thread(new Work(t1),"线程t2");
Thread t3 = new Thread(new Work(t2),"线程t3");
t1.start();
t2.start();
t3.start();
}
static class Work implements Runnable{
private Thread beforeThread;
public Work(Thread beforeThread) {
this.beforeThread = beforeThread;
}
@Override
public void run() {
if(beforeThread!=null) {
try {
//某线程调用该方法,会让其他线程处于等待状态,让其运行完毕,再执行其他线程.
beforeThread.join();
System.out.println("Thread start:"+Thread.currentThread().getName());
}catch(Exception e){
e.printStackTrace();
}
}else {
System.out.println("Thread start:"+Thread.currentThread().getName());
}
}
}
}
9 (面试题)方法2:使用CountDownLatch
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。
import java.util.concurrent.CountDownLatch;
public class Thread2 {
public static void main(String[] args) {
CountDownLatch c1 = new CountDownLatch(0);//计数器为0
CountDownLatch c2 = new CountDownLatch(1);//计数器为1
CountDownLatch c3 = new CountDownLatch(1);//计数器为1
Thread t1 = new Thread(new Work(c1, c2),"线程t1");
//c1为0,t1线程可以执行。t1线程的计数器 c2 减1
Thread t2 = new Thread(new Work(c2, c3),"线程t2");
//t1的计数器c2为0时,t2才能执行。t2的计数器c3减1
Thread t3 = new Thread(new Work(c3, c3),"线程t3");
//t3的计数器c3为0时,t3才能执行
t1.start();
t2.start();
t3.start();
}
//定义Work线程类,需要传入开始和结束的CountDownLatch参数
static class Work implements Runnable {
private CountDownLatch c1;
private CountDownLatch c2;
public Work(CountDownLatch c1, CountDownLatch c2) {
super();
this.c1 = c1;
this.c2 = c2;
}
@Override
public void run() {
try {
//当某个线程调用CountDownLatch对象的await方法时,将会阻塞,直到计数器的值变成0才放行。
c1.await();
System.out.println("thread start:" + Thread.currentThread().getName());
c2.countDown(); //本线程计数器减 1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}