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)自学:使用阻塞队列实现生产者消费者模式
02-12 20:31