线程同步

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)自学:使用阻塞队列实现生产者消费者模式
计算机