线程通信
线程通信-打印机打印实例
线程通信基本实现
实例:打印机打印
实现功能:不断输⼊不断输出
总结:需要给输⼊任务和输出任务同时加⼀把锁,保证两个任务之间是同步的给两个任务加⼀把锁:可以是desc或者Object.class
不建议使⽤Object.class:由于Object的使⽤范围太⼤,可能造成不必要的错误.desc合适,因为他只被当前的两个任务共享.
注意:对于当前的情况只给⼀个线程加锁,⽆法实现两个线程的同步.
线程通信功能优化
实例:打印机打印
功能:对⼀次输⼊⼀次输出代码的改进
总结:进⾏了代码优化
⾯向对象的精髓:谁的活⼉谁⼲,不是你的活⼉不要⼲
将数据准备的活⼉从输⼊任务输出任务提出来,放⼊数据类Desc2
package thread02;
public class Demo1 {
public static void main(String[] args) {
Desc desc = new Desc();
Input input = new Input(desc);
Output output = new Output(desc);
Thread t1 = new Thread(input);
Thread t2 = new Thread(output);
t1.start();
t2.start();
}
}
class Input implements Runnable{
Desc desc;
int i;
public Input(Desc desc){
this.desc = desc;
}
@Override
public void run() {
while (true){
synchronized (desc){
if (i==0){
desc.setData("zhangsan","男");
}else {
desc.setData("lisi","女");
}
i++;
i = i%2;
}
}
}
}
class Output implements Runnable{
Desc desc;
public Output(Desc desc){
this.desc = desc;
}
@Override
public void run() {
while (true){
synchronized (desc){
desc.getData();
}
}
}
}
class Desc{
String name;
String sex;
boolean flag = false;
public synchronized void setData(String name,String sex){
if (flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
System.out.println(name+"读入成功");
flag = !flag;
notify();
}
public synchronized void getData(){
if (!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("name:"+name+" sex:"+sex);
flag = !flag;
notify();
}
}
⽣产者消费者模式
⽣产者消费者问题是研究多线程程序经典问题之⼀,它描述是有⼀块缓冲区作为仓库,⽣产者可以将产品放⼊仓库,消费者则可以从仓库中取⾛产品。在Java中⼀共有
四种⽅法⽀持同步,其中前三个是同步⽅法,⼀个是管道⽅法。
(1)Object的wait() / notify()⽅法 (2)Lock和Condition的await() / signal()⽅法
(3)BlockingQueue阻塞队列⽅法 (4)PipedInputStream /
PipedOutputStream
单生产者单消费者
单生产者单消费者问题的代码展示与上述打印机的案例类似,单生产者的模式在于一个生产者要先生产一个产品之后,才能由消费者去消费一个对应的产品,要注意控制线程的执行流程以及共用一个变量的原则。
多⽣产者多消费者
总结:将单⽣产者单消费者代码不做更改,直接再添加⼀个⽣产线程,⼀个消费线程,就形成了多⽣产者多消费者多消费者.
出现的错误1
错误描述:当有两个⽣产线程,两个消费线程同时存在的时候,有可能出现⽣产⼀次,消费多次或者⽣产多次消费⼀次的情况.
原因:当线程被重新唤醒之后,没有判断标记,直接执⾏了下⾯的代码
解决办法:将标记处的if改成while
出现的错误2
问题描述:继续运⾏程序,会出现死锁的情况(4个线程同时处于等待状态)
原因:唤醒的是本⽅的线程,最后导致所有的线程都处于等待状态.
解决办法:将notify改成notifyAll.保证将对⽅的线程唤醒
Lock锁
为什么使⽤Lock锁?
在我们使⽤synchronized进⾏同步的时候,锁对象是Object类的对象,使⽤的wait,notify⽅法都来⾃Object类,但是咱们知道并不是所有的对象都会⽤到同步,所以这样⽤法不太合理,⽽且锁相关的功能很多,Lock就是将锁⾯向对象的结果.不光是将锁⾯向对象了,同时将wait,notify等⽅法也做了⾯相对象处理.形成了Condition接⼝.当我们想实现多⽣产者多消费者模式时,可以使⽤Lock实现同步,同时配合Condition接⼝实现唤醒等待.
//创建锁对象
Lock lock = new ReentrantLock();
//⽤于⽣产任务的Condition
Condition proCon = lock.newCondition();
//⽤于消费任务的Condition
Condition conCon = lock.newCondition();
⽐较synchronized和Lock
1.synchronized:从jdk1.0就开始使⽤的同步⽅法-称为隐式同步
synchronized(锁对象){//获取锁 我们将锁还可以称为锁旗舰或者监听器
同步的代码
}//释放锁
2.Lock:从jdk1.5开始使⽤的同步⽅法-称为显示同步
原理:Lock本身是接⼝,要通过他的⼦类创建对象⼲活⼉
常⽤⼦类:ReentrantLock
使⽤过程:
⾸先调⽤lock()⽅法获取锁
进⾏同步的代码块⼉
使⽤unlock()⽅法释放锁
使⽤的场景:
当进⾏多⽣产者多消费者的功能时,使⽤Lock,其他的都使⽤synchronized
使⽤效率:Lock⾼于synchronized
⽐较Object多wait,notify和Condition的await,signal
示例代码
package morning;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
public static void main(String[] args) {
Product product = new Product();
Produce produce = new Produce(product);
Consume consume = new Consume(product);
Thread thread1 = new Thread(produce);
Thread thread2 = new Thread(produce);
Thread thread3 = new Thread(consume);
Thread thread4 = new Thread(consume);
thread1.start();
thread3.start();
thread2.start();
thread4.start();
}
}
class Produce implements Runnable{
private Product product;
public Produce(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.setProduce("旺旺碎冰冰", 5);
}
}
}
class Consume implements Runnable{
private Product product;
public Consume(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
product.getConsume();
}
}
}
class Product{
private String name;
private double price;
private int count;
private boolean flag = false;
Lock lock = new ReentrantLock();
Condition proCon = lock.newCondition();
Condition conCon = lock.newCondition();
public void setProduce(String name,double price){
try{
lock.lock();
while (flag == true){
try {
proCon.await();
}catch (InterruptedException e){
e.printStackTrace();
}
}
this.name = name;
this.price = price;
count++;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" ⽣产 了:"+this.name+" 产品的数量:"+this.count+" 价 格:"+this.price);
flag = !flag;
conCon.signal();
}finally {
lock.unlock();
}
}
public void getConsume(){
try {
lock.lock();
while (flag == false){
try {
conCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 消费 了:"+this.name+" 价格:"+this.price);
//count--;
flag = !flag;
proCon.signal();
}finally {
lock.unlock();
}
}
public Product() {
}
public Product(String name, double price, int count) {
this.name = name;
this.price = price;
this.count = count;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", price=" + price +
", count=" + count +
'}';
}
}
唤醒等待机制
⽅法简介
Object类中⼏个⽅法如下:
wait()
等待,让当前的线程,释放⾃⼰持有的指定的锁标记,进⼊到等待队列。
等待队列中的线程,不参与CPU时间⽚的争抢,也不参与锁标记的争抢。
notify()
通知、唤醒。唤醒等待队列中,⼀个等待这个锁标记的随机的线程。
被唤醒的线程,进⼊到锁池,开始争抢锁标记。
notifyAll()
通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。
被唤醒的线程,进⼊到锁池,开始争抢锁标记。
wait和sleep的区别
sleep()⽅法,在休眠时间结束后,会⾃动的被唤醒。 ⽽wait()进⼊到的阻塞态,需要被notify/notifyAll⼿动唤醒。
wait()会释放⾃⼰持有的指定的锁标记,进⼊到阻塞态。sleep()进⼊到阻塞态的时候,不会释放⾃⼰持有的锁标记。
注意事项
⽆论是wait()⽅法,还是notity()/notifyAll()⽅法,在使⽤的时候要注意,⼀定要是⾃⼰持有的锁标记,才可以做这个操作。否则会出现IllegalMonitorStateException 异常。
为什么wait,notify⽅法要使⽤锁调⽤?
死锁
出现的情况有两种
所有的线程处于等待状态
⼤家都处于等待状态,没有⼈获取cpu使⽤
锁之间进⾏嵌套调⽤
多个线程, 同时持有对⽅需要的锁标记, 等待对⽅释放⾃⼰需要的锁标记。此时就是出现死锁。 线程之间彼此持有对⽅需要的锁标记, ⽽不进⾏释放, 都在等待。
线程其他内容
线程的停⽌
/*
* 线程的停⽌:3种
* 1.通过⼀个标识结束线程
* 2.调⽤stop⽅法---因为有固有的安全问题,所以系统不建议使⽤.
* 3.调⽤interrupt⽅法----如果⽬标线程等待很⻓时间(例如基于⼀个条件变量),则应使⽤ interrupt ⽅法来中断该等待。
*/
线程的休眠
线程休眠, 就是让当前的线程休眠指定的时间。 休眠的线程进⼊到阻塞状态, 直到休眠结束。 阻塞的线程, 不参与CPU时间⽚的争抢。
注: 线程休眠的时间单位是毫秒。(Thread.sleep())
线程的合并
将⼀个线程中的任务, 合并⼊到另外⼀个线程中执⾏, 此时, 合并进来的线程有限执⾏。 类似于: 插队。
注意:优先级只⽐main线程的⾼.对其他的线程没有影响.
必须先开启线程在进行合并
比如:
// 先开启
vip.start();
// 再合并
try {
vip.join();
} catch (InterruptedException e) {
}
线程的优先级设置
设置线程的优先级, 可以决定这个线程能够抢到CPU时间⽚的概率。 线程的优先级范围在 [1, 10], 默认的优先级是5。 数值越⾼, 优先级越⾼。 但是要注意, 并不是优先级⾼的线程⼀定能抢到CPU时间⽚, 也不是优先级的线程⼀定抢不到CPU时间
⽚。 线程的优先级只是决定了这个线程能够抢到CPU时间⽚的概率。 即便是优先级最低的线程, 依然可以抢到CPU时间⽚。
// 实例化两个线程, 处理的逻辑完全相同
Thread thread0 = new Thread(runnable, "t0");
Thread thread1 = new Thread(runnable, "t1");
// 设置线程的优先级, 必须在这个线程启动之前
thread0.setPriority(1);
thread1.setPriority(10);
守护线程
守护线程, ⼜叫后台线程。 是⼀个运⾏在后台, 并且会和前台线程争抢CPU时间⽚的线程。
守护线程依然会和前台线程争抢CPU时间⽚, 实现并发的任务。
在⼀个进程中, 如果所有的前台线程都结束了, 后台线程即便任务没有执⾏结束, 也会⾃动结束
// 将⼀个线程设置为守护线程
thread.setDaemon(true);
// 开启线程
thread.start();
线程池
线程池的简介
线程池, 其实就是⼀个容器, ⾥⾯存储了若⼲个线程。
使⽤线程池, 最主要是解决线程复⽤的问题。 之前使⽤线程的时候, 当我们需要使⽤⼀个线程时, 实例化了⼀个新的线程。 当这个线程使⽤结束后, 对这个线程进⾏销毁。 对于需求实现来说是没有问题的, 但是如果频繁的进⾏线程的开辟和销毁,其实对于CPU来说, 是⼀种负荷, 所以要尽量的优化这⼀点。
使⽤复⽤机制解决这个问题。 当我们需要使⽤到⼀个线程的时候, 不是直接实例化, ⽽是先去线程池中查找是否有闲置的线程可以使⽤。 如果有, 直接拿来使⽤; 如果没有, 再实例化⼀个新的线程。 并且, 当这个线程使⽤结束后, 并不是⻢上销毁, ⽽是将其放⼊到线程池中, 以便下次继续使⽤。
线程池的开辟
在Java中, 使⽤ThreadPoolExecutor类来描述线程池, 在这个类的对象实例化的时候, 有⼏个常⻅的参数:
BlockingQueue
ArrayBlockingQueue
LinkedBlockingQueue
SynchronouseQueue
RejectedExecutionHandler
ThreadPoolExecutor.AbortPolicy : 丢弃新的任务,并抛出异常
RejectedExecutionException
ThreadPoolExecutor.DiscardPolicy : 丢弃新的任务,但是不会抛出异常
ThreadPoolExecutor.DiscardOldestPolicy : 丢弃等待队列中最早的任务
ThreadPoolExecutor.CallerRunsPolicy : 不会开辟新的线程,由调⽤的线程来处理
线程池的⼯作原理
线程池中的所有线程, 可以分为两部分: 核⼼线程 和 临时线程
核⼼线程:
核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当线程池需要被销毁的时候, 他们才会被销毁。
临时线程:
就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销
毁。
任务分配逻辑:
- 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
- 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊
等待队列。 - 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新
的任务。 - 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会
触发拒绝访问策略。
线程池的常⽤⽅法
线程池的⼯具类
线程池的开辟, 除了可以使⽤构造⽅法进⾏实例化, 还可以通过Executors⼯具类进⾏获取。 实际应⽤中, ⼤部分的场景下, 可以不⽤前⾯的构造⽅法进⾏线程池的实例化, ⽽是⽤Executors⼯具类中的⽅法进⾏获取。
, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销
毁。
任务分配逻辑:
- 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
- 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊
等待队列。 - 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新
的任务。 - 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会
触发拒绝访问策略。