目录
wait 和 notify 的调用必须是同一个对象调用。谁让他等待了,谁才能让他唤醒。解铃还须系铃人。
线程同步问题
多个线程同时访问一个资源
原因:
多个线程的执行是抢占式的,当一个线程执行方法时,可能会被另一个线程抢占CPU,当前线程的操作不能完整的执行,导致数据出现问题。
public void transfer(int from,int to,int money){
//一个线程扣除一个账户一定金额,准备给另一个账户加钱
//被其他线程抢占CPU执行,其他线程执行完转账后,前面线程扣的钱还没有加
//统计钱总数就出现问题
accounts[from] -= money;
System.out.println("从"+from+"转到"+to+"账户"+money);
accounts[to] += money;
System.out.println("银行的总账是:" + sum());
}
知识兔解决方法:
让一个线程能完整的执行完任务后,另一个线程再执行
上锁机制:
1. 同步代码块
2. 同步方法
3. 同步锁
同步代码块
可以对一段代码上锁
语法:
synchronized(锁对象){
需要上锁的代码
}
知识兔锁对象:
任意的成员变量对象都可以作为锁,因为在Object类中定义了锁的方法。对象必须是成员对象
public void transfer(int from,int to,int money){
synchronized (lock) {
accounts[from] -= money;
System.out.println("从"+from+"转到"+to+"账户"+money);
accounts[to] += money;
System.out.println("银行的总账是:" + sum());
}
}
知识兔原理:
当一个线程进入同步块后,JVM启动一个Monitor(监视器)对同步块进行监控,如果另一个线程想进入这个同步块,监视器会拒绝,当前面线程执行完代码,释放锁后,监视器才会让其他线程进入。
知识兔锁类:
锁普通方法可以锁对象,锁静态方法要锁类
public static void transfer(int from,int to,int money){
synchronized (本类.class) {
accounts[from] -= money;
System.out.println("从"+from+"转到"+to+"账户"+money);
accounts[to] += money;
System.out.println("银行的总账是:" + sum());
}
}
同步方法
public synchronized 返回值 方法名(参数){
代码
}
知识兔面试题:同步块和同步方法的区别
1)锁的粒度不同
同步块粒度小,可以只锁一段代码
同步方法粒度大,锁的是整个方法
2)锁对象不同
同步块任意成员对象都可以作为锁
同步方法锁对象是this
知识兔同步锁
jdk1.5出现的,类似同步块,但性能更高,功能更强。
Lock接口
实现类:
ReadLock 读锁
WriteLock 写锁
ReadWriteLock 读写锁
ReentrantLock 重入锁
知识兔用法
1)创建ReentrantLock 的成员变量
Lock必须是成员变量才行,局部变量还是其他的都不行
2)上锁 ,调用lock()
3)释放锁,调用unlock(),注意unlock必须执行,否则就会死锁
锁.lock();
try{
需要上锁代码
}finally{
锁.unlock();
}
性能:
同步锁 > 同步代码块 > 同步方法
总结:
锁机制会提高多线程情况下数据安全性,但是会降低程序的性能
线程的死锁
可能出现的原因:
1)使用同步锁之后没有手动释放锁
2)两个线程互相合并对方
3)两个线程都需要对方的锁,又都持有对方的锁
两个方法、两个锁、两个同步块相互嵌套、两个线程
锁1
锁2
方法1(){
sync(锁1){
....
sync(锁2){
}
}
}
方法2(){
sync(锁2){
....
sync(锁1){
}
}
}
知识兔线程的等待和通知
Object类的方法:
等待:
void wait() 让当前线程进入等待状态
void wait(long time) 让当前线程等待一定时间,时间过后线程恢复执行
知识兔通知:
void notify() 通知等待的其中一个线程,让其恢复执行
void notifyAll() 通知所有等待的线程
知识兔注意:
1)上面的四个方法必须是锁对象,如果不是就会抛出异常IllegalMonitorStateException。
2)通知线程的锁对象,必须是让线程进行等待的锁对象
知识兔机制:
线程执行到同步块或同步方法时,监视器会对线程进行监视,当调用wait或notify时,JVM会检查调用方法的对象是否是同步块或方法的锁对象,如果不是就抛出异常。
知识兔面试题:wait和sleep的区别
1)wait时线程会释放锁,sleep时线程不会释放锁
2)调用方式:wait可以由任意锁对象,sleep是通过Thread类调用
3)wait可以不设置时间,sleep必须设置睡眠时间
4)wait可以被通知,sleep必须等睡眠时间结束
知识兔生产者消费者设计模式
不是GOF23模式之一,是和线程相关的模式
在线程世界中生产和消费的是数据,有些线程用于生产数据就是生产者,有些线程用于使用数据就是消费者,生产者生产数据的速度和消费者消费数据的速度会出现不一致,生产者的速度过快会浪费系统资源,消费者速度过快会浪费系统资源和用户时间,生产者和消费者模式主要用于协调生产者线程和消费者线程的速度。
知识兔几个重要点:
1)缓冲区
2)线程
3)等待
4)通知
知识兔思路:
首先会建立缓冲区用于存放数据,生产者线程生产的数据会放入缓冲区,如果缓冲区满了,就让生产者线程等待,通知所有消费者线程来消费,如果缓冲区空了,就让消费者线程等待,通知所有生产者来生产。
知识兔/** * 包子类 * @author Administrator * */public class Baozi { private int id; public Baozi(int id) { this.id = id; } @Override public String toString() { return "Baozi [id=" + id + "]"; }}---------------------------------------------------/** * 包子仓库 * @author Administrator * */public class BaoziStore { //缓冲区上限 private static final int MAX_COUNT = 50; //包子缓冲区 private List<Baozi> baozis = new ArrayList<>(); //做包子 public synchronized void makeBaozi(){ //如果缓冲区满了,就让生产者线程等待 if(baozis.size() == MAX_COUNT){ System.out.println("仓库满了,生产者等待"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ //如果缓冲区没满,通知生产者继续做包子,以及消费者来吃包子 this.notifyAll(); } //创建包子,保存到缓冲区 Baozi baozi = new Baozi(baozis.size() + 1); System.out.println("生产者做了"+baozi); baozis.add(baozi); } //拿包子 public synchronized void takeBaozi(){ //如果缓冲区空了,就让消费者线程等待 if(baozis.size() == 0){ System.out.println("仓库空了,消费者等待" + Thread.currentThread().getName()); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ //如果缓冲区没满,通知生产者继续做包子,以及消费者来吃包子 this.notifyAll(); } //从缓冲区删除一个包子 if(baozis.size() > 0){ Baozi baozi = baozis.remove(baozis.size() - 1); System.out.println(Thread.currentThread().getName()+"吃了"+baozi); } }}----------------------------------------------------------public class BaoziTest { public static void main(String[] args) { //创建包子仓库 BaoziStore store = new BaoziStore(); //创建生产者线程生产100个包子 new Thread(()->{ for(int i = 0;i < 100;i++){ store.makeBaozi(); } }).start(); //创建100个消费者线程吃1个包子 for(int i = 0;i < 100;i++){ new Thread(()->{ store.takeBaozi(); }).start(); } }}
线程池
线程是一种系统级的资源,创建和销毁是需要消耗系统资源的,如果大量使用线程,推荐使用线程池,线程池能够对线程进行回收利用,从而节约系统资源。一般的线程执行完后,就进入死亡状态,被回收;在线程池中的线程执行完任务后,会进入等待状态,直到有新的任务,再通知线程执行。
API:
Executor 接口
execute(Runnable runnable) 在线程池中获得线程执行任务
ExecutorService 接口 继承Executor
shutdown() 停止,等待所有线程执行完shutdownNow() 停止,强制停止所有线程
Executors线程池工具类
帮助创建各种线程池newCachedThreadPool() 创建长度不限的线程池newFixedThreadPool(int size)创建长度固定的线程池 size是线程的最大数量 前两种的区别: 长度不限的线程池,可能在并发量大的情况下创建很多线程,对系统的压力比较大;长度有限的线程池,可以控制并发量 size 线程数量应该是CPU的核心数 * N (N大于等于1取决于任务执行时间和并发量) 内核数:Runtime.getRuntime().availableProcessors() newSingleThreadExecutor() 创建单一线程池
作业:
1)使用继承Thread、Runnable和各种线程池启动线程2)编写线程安全的单例模式3)使用生产者消费者模拟12306网站,4个服务器每个提供50张票,100个消费者每人买两张票4)自学:使用阻塞队列实现生产者消费者模式