线程通信
(1)概述(通过图介绍)
(2)案例
//奶箱类(锁对象)
//1. 成员变量: 奶箱存入的奶的次数int milk, 奶箱的状态 boolean state = true/false;
//2. 构造方法: 无
//3. 成员方法: 存奶操作 void put(int milk) 取奶操作 void get()
public class Box {
//成员变量
private int milk;
//刚开始的时候,奶箱里面没有奶
private boolean state = false;
//存奶的操作,synchronized 请问锁对象是谁? this -> Box 的对象
public synchronized void put(int milk){
//1. 判断状态,是否等待
if (state == true){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2. 存奶,输出结果
this.milk = milk;
System.out.println("生产者存奶,次数:"+milk);
//3. 状态修改, 因为生产者已经存奶,状态修改为 true
state = true;
//4. 唤醒所有的线程,准备抢!
this.notifyAll();
}
//取奶的操作 synchronized 请问锁对象是谁? this -> Box 的对象
public synchronized void get(){
//1. 判断是否需要进入等待状态
if (state == false){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2. 取奶
System.out.println("消费者取奶,次数:"+milk);
//3. 修改状态,奶已经被取走了,状态是 false
state = false;
//4. 唤醒所有线程,准备抢!
this.notifyAll();
}
}
//生产者
public class Producer implements Runnable{
//需要传递共享锁对象! Box 对象唯一
private Box box;
//构造方法
public Producer(Box box) {
this.box = box;
}
@Override
public void run() {
//生产者生成 30次的奶
for (int i = 1; i < 31; i++) {
//调用方法
box.put(i);
}
}
}
//消费者
public class Customer implements Runnable{
//需要传递共享锁对象! Box 对象唯一
private Box box;
public Customer(Box box) {
this.box = box;
}
@Override
public void run() {
//死循环不断的取奶
while(true){
box.get();
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
// (1). 创建唯一的奶箱对象(唯一的锁对象)
Box box = new Box();
// (2). 将唯一的锁对象,分配给生产者和消费者
Thread t1 = new Thread(new Producer(box));
Thread t2 = new Thread(new Customer(box));
// (3). 启动线程
t1.start();
t2.start();
}
}
(3)阻塞队列
a) 创建对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1); //参数:容量大小
b) 存放数据
queue.put("Hello");
c) 获取数据
String s = queue.take();
d) 容量限制
容量 >= put 数目 >= take 数目
线程生命周期
六种生命状态:
(1)NEW 新建
(2)RUNNABLE 可运行
(3)BLOCKED 阻塞
(4)WAITING 等待
(5)TIMED_WAITING 计时等待
(6)TERMINATED 销毁
线程池
(1)默认线程池
a) 步骤:
aa) 创建线程池对象
bb) 提交线程任务
cc) 销毁线程池
b) 案例
//创建默认的线程池
public class Test01 {
public static void main(String[] args) {
// (1). 创建线程池对象
ExecutorService service = Executors.newCachedThreadPool();
// (2). 提交线程任务
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
// (3). 销毁线程池
service.shutdown();
}
}
(2)创建制定上限的线程池
a) 步骤:
aa) 创建线程池对象(参数:线程池的线程最大值)
bb) 提交线程任务
cc) 销毁线程池
b) 案例
//创建指定上限线程数目的线程池
public class Test02 {
public static void main(String[] args) {
// (1). 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(3);
// (2). 提交线程任务
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
service.submit(()-> System.out.println(Thread.currentThread().getName()+"正在执行任务"));
// (3). 销毁线程池
service.shutdown();
}
}
(3)自定义线程池
a) 步骤:
aa) 创建线程池对象
bb) 提交线程任务
cc) 销毁线程池
b) 案例
//创建线程池的对象(共计7个参数)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, //参数1: 核心线程数目,不能小于0
5, //参数2: 最大线程数目, 大于0, 最大数量>=核心线程数量
2, //参数3: 空闲线程最大存活时间, 不能小于0
TimeUnit.SECONDS, //参数4: 时间单位
new ArrayBlockingQueue<>(10), //参数5: 任务队列,不能为null
Executors.defaultThreadFactory(), //参数6: 创建线程工厂,不能为null
new ThreadPoolExecutor.AbortPolicy() //参数7: 任务的拒绝策略, 不能为null
);
//提交线程任务
pool.submit(()-> System.out.println(Thread.currentThread().getName()+"执行了"));
pool.submit(()-> System.out.println(Thread.currentThread().getName()+"执行了"));
pool.submit(()-> System.out.println(Thread.currentThread().getName()+"执行了"));
c) 任务拒绝策略
aa) ThreadPoolExecutor.AbortPolicy //丢弃任务并且抛出 RejectedExecutionException。 (默认的策略)
bb) ThreadPoolExecutor.DiscardPolicy //丢弃任务, 但是不会抛出异常。 这个是不推荐的做法
cc) ThreadPoolExecutor.DiscardOldestPolicy //抛弃队列当中等待最久的任务, 然后把当前任务加入队列当中。
dd) ThreadPoolExecutor.CallerRunsPolicy //调用任务的 run() 方法, 绕过线程池直接运行。
原子性
概述:一组操作当中, 要么同时成功, 要么同时失败。
(1)volatile 关键字
a) 概述:强制要求,线程每次使用变量,都重新获取最新值
b) volatile 能不能保证原子性:因为 volatile 关键字主要是用于获取最新数据。对于多个线程而言, 抢到了 共享数据之后,如果都没有进行 重新赋值回去的操作,共享数据当中还是最新的值。 现在多个线程同时对共享数据赋值,那么将无法保证原子性。
c) 案例
//银行类
public class Bank {
//定义静态共享数据
public static int money = 10;
//public static volatile int money = 10;
}
//男孩线程
public class Boy extends Thread{
@Override
public void run() {
try {
//先让男孩睡10毫秒
Thread.sleep(10);
//修改银行存款
Bank.money = 9;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//女孩线程
public class Girl extends Thread{
@Override
public void run() {
//循环判断是否是10万,是10万就继续
while(Bank.money == 10){
}
System.out.println("结婚基金已经不是10万啦");
}
}
//测试类
public class Test {
public static void main(String[] args) {
Boy boy = new Boy();
boy.start();
Girl girl = new Girl();
girl.start();
}
}
(2)原子性案例演示(提出问题)
//线程类
public class XianCheng implements Runnable{
//定义成员变量
private int count;
public XianCheng(int count) {
this.count = count;
}
@Override
public void run() {
//循环打印输出
for (int i = 0; i < 100; i++) {
System.out.println("输出count的数据值:"+count);
count++; //在这里实际上是3个步骤
}
}
//1. 获取共享数据交给 单独的子线程任务栈
//2. 在子线程任务栈当中,自增
//3. 将自增之后的结果,赋值给共享数据
}
//测试类
public class Test {
public static void main(String[] args) {
//开启100个线程
XianCheng xc = new XianCheng(1);
//循环100次,使用共享数据 xc
for (int i = 0; i < 100; i++) {
Thread tt = new Thread(xc);
tt.start();
}
}
}
问题分析:自增操作,不是原子的,分为了三个步骤,所以在执行这三个步骤的时候,CPU资源很可能被其他线程抢占,如果其他线程访问了该资源,就很可能出现错误
(3)原子类 AtomicInteger
a) 构造方法
方法 | 备注 |
---|---|
public AtomicInteger() | 初始化值为0的原子类型 Integer |
public AtomicInteger(int initialValue) | 给出一个具体初始值的,原子类型Integer |
b) 常用方法
方法 | 备注 |
---|---|
int get() | 获取值 |
int getAndIncrement() | 将当前的值,自增加1. 返回的是 之前的数据值 |
int incrementAndGet() | 将当前的值,自增加1. 返回的是 自增之后的数据值 |
int addAndGet(int data) | 将当前参数当中的值, 与构造方法当中的值, 相加,返回的相加之后的和 |
int getAndSet(int value) | 先获取原来构造方法当中的值, 然后将参数当中的值设置进去, 最终返回的是之前构造方法的值 |
c) 内存原理(自旋锁+CAS算法 CAS:内存值,旧的预期值A,要修改的值B)
aa) 当旧的预期值A == 内存值 //此时修改成功, 将V改为B —> 如果旧值和内存值相等,表示当前的数据值,没有被其他线程操作过。
bb) 当旧的预期值A != 内存值 //此时修改失败, 不做任何操作 --> 如果旧值和内存值不相等,表示当前的数据值,已经被其他线程操作过。【这里就需要重新获取(自旋)】
cc) 并重新获取现在的最新值 (这个重新获取的动作,就是自旋
悲观锁和乐观锁
synchroized 和 CAS 的区别
(1)相同点:
都可以在多线程的情况下, 保证共享数据的安全性,原子性。
(2)不同点:
A. 悲观锁(synchroized) 总是从最坏角度思考, 所有的人都有可能改变原始数据,每次使用,都会上锁。 //不管有没有改变数据,统一上锁。
B. 乐观锁(CAS) 从乐观的角度出发, 假设别人使用不会修改,不会上锁。 只有共享数据改变的情况下,会检查数据,进行修改。 //会对数据进行检查,是否有变化
并发工具类
(1)Hashtable
a) Hashtable和HashMap的区别
aa) 安全性:Hashtable安全性更高,HashMap安全性低
bb) 效率:HashMap效率高,Hashtable效率低
原因:Hashtable底层是同步锁,HashMap底层没有上锁
(2)ConcurrentHashMap
a) 概述:既保证了安全性,也保证了访问的高效
b) 底层原理(为什么能实现安全又高效)
JDK1.7:底层是一个长度16的数组(不可以扩容),数组中每个元素存放了一个小数组地址。在线程访问的时候,只锁住1/16的小数组,也就是说,最多允许有16个线程同时访问
JDK1.8:底层采用的是数组+链表+红黑树的结构存储。在线程操作的过程中,采用同步锁synchronized去锁住数组的一个元素(元素中包含的是链表或者红黑树),结合CAS算法(乐观锁)和悲观锁保证线程安全。(可以扩容,初始大小为16,后续扩容之后可以允许超过16个线程访问)
(3)CountDownLatch
a) 概述:让其中的一条线程, 等待其他所有线程执行完毕之后,再去执行。(其他线程执行完毕之后,它再执行)
b) 常用方法
方法 | 备注 |
---|---|
CountDownLatch(数字) | 构造方法。创建一个需要等待n条线程的示例(参数的数字表示需要等待的线程数) |
await() | 等待方法 |
countDown() | 减少方法 |
c) 案例
//下载线程
public class XiaZaiBa implements Runnable{
private int start;
private int end;
private CountDownLatch latch;
public XiaZaiBa(int start, int end, CountDownLatch latch) {
this.start = start;
this.end = end;
this.latch = latch;
}
@Override
public void run() {
for (int i = start; i <= end; i++) {
String name = Thread.currentThread().getName();
System.out.println(name+"正在下载:"+i);
}
//当整个线程下载完毕之后,需要减少。
latch.countDown();
}
}
//展示结果的线程
public class ShowResult implements Runnable {
private CountDownLatch latch;
public ShowResult(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("恭喜你,下载完毕,快去看看吧");
}
}
//测试类
public class Test {
public static void main(String[] args) {
//需要创建 CountDownLatch 对象
CountDownLatch latch = new CountDownLatch(3);
//创建3个下载线程的对象
XiaZaiBa xzb1 = new XiaZaiBa(1,30,latch);
XiaZaiBa xzb2 = new XiaZaiBa(31,60,latch);
XiaZaiBa xzb3 = new XiaZaiBa(61,100,latch);
//创建最终的线程
ShowResult show = new ShowResult(latch);
// --------
//启动线程
Thread t1 = new Thread(xzb1);
Thread t2 = new Thread(xzb2);
Thread t3 = new Thread(xzb3);
Thread t4 = new Thread(show);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
(4)Semaphore
a) 概述:可以控制特定资源的线程访问数量。 (交通道路上面,控制一次有多少量车通过)
b) 常用方法
方法 | 备注 |
---|---|
Semaphore(数字) | 构造方法,每次运行n个线程,参数中的数字代表运行的线程数量(最多允许发放几个令牌) |
acquire() | 获取令牌 |
release() | 归还令牌 |
c) 案例
//线程
public class MyThread implements Runnable{
private Semaphore sema;
public MyThread(Semaphore sema) {
this.sema = sema;
}
@Override
public void run() {
try {
//获取通行证
sema.acquire();
String name = Thread.currentThread().getName();
System.out.println(name + "拿到了通行证");
Thread.sleep(50);
System.out.println(name + "归还通行证");
//归还通行证
sema.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
//最多允许3条线程
Semaphore sema = new Semaphore(3);
MyThread mt = new MyThread(sema);
//开启多条线程执行
for (int i = 1; i < 101; i++) {
Thread t = new Thread(mt);
t.setName("线程-"+i);
t.start();
}
}
}