1.线程的生命周期
新建、就绪、运行、阻塞、死亡
2.线程的安全问题
案例:电影院卖票
1.定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
2. 在Ticket类中重写run()方法实现卖票,代码步骤如下
A:判断票数大于0,就卖票,并告知是哪个窗口卖的
B:票数要减1
C:卖光之后,线程停止
3. 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
A:创建Ticket类的对象
B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
C:启动线程
public class Ticket implements Runnable{
private int ticketCount = 100;
@Override
public void run() {
while (ticketCount > 0){
// 睡眠一会 因为卖票需要时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数减1
ticketCount--;
// 打印当前线程名称 和 剩余票数
String name = Thread.currentThread().getName();
System.out.println("当前线程名字:"+ name + "卖了一张票,剩余:" + ticketCount +"张票");
}
}
public static void main(String[] args) {
// 1.创建Ticket类的对象
Ticket ticket = new Ticket();
// 2.创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
// 3.启动线程
t1.start();
t2.start();
t3.start();
}
}
卖票出现了问题
1.相同的票出现了多次
2.出现了负数的票
问题原因
线程执行的随机性导致的
为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)
1.多线程操作共享数据 如何解决多线程安全问题呢?
2.基本思想:让程序没有安全问题的环境 怎么实现呢?
3.把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
4.Java提供了同步代码块的方式来解决
3.线程安全问题的三种解决方案
1.同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
1.格式: synchronized(任意对象:多个线程的锁对象必须唯一、必须一致) { 多条语句操作共享数据的代码 }
2.默认情况是打开的,只要有一个线程进去执行代码了,锁就会关闭
3.当线程执行完出来了,锁才会自动打开
同步的好处和弊端
1.好处:解决了多线程的数据安全问题
2.弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
public class Ticket implements Runnable{
private int ticketCount = 100;
private Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj) {
if(ticketCount <= 0) break;
// 睡眠一会 因为卖票需要时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数减1
ticketCount--;
// 打印当前线程名称 和 剩余票数
String name = Thread.currentThread().getName();
System.out.println("当前线程名字:" + name + "卖了一张票,剩余:" + ticketCount + "张票");
}
}
}
public static void main(String[] args) {
// 1.创建Ticket类的对象
Ticket ticket = new Ticket();
// 2.创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
// 3.启动线程
t1.start();
t2.start();
t3.start();
}
}
2.同步方法
同步方法:就是把synchronized关键字加到方法上
1.格式: 修饰符 synchronized 返回值类型 方法名(方法参数) { } 同步代码块和同步方法的区别:
2.同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
3. 同步代码块可以指定锁对象,同步方法不能指定锁对象
同步方法的锁对象是什么呢?
this
同步静态方法:就是把synchronized关键字加到静态方法上
1.格式: 修饰符 static synchronized 返回值类型 方法名(方法参数) { }
2.同步静态方法的锁对象是什么呢?
类名.class
以下时同步方法实现
public class Ticket implements Runnable{
private int ticketCount = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
if ("窗口1".equals(Thread.currentThread().getName())) {
boolean b = sale1();
if (b) break;
}
if ("窗口2".equals(Thread.currentThread().getName())) {
boolean b = sale1();
if (b) break;
}
}
}
public synchronized boolean sale1(){
if (ticketCount <= 0) return true;
// 睡眠一会 因为卖票需要时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数减1
ticketCount--;
// 打印当前线程名称 和 剩余票数
String name = Thread.currentThread().getName();
System.out.println("当前线程名字:" + name + "卖了一张票,剩余:" + ticketCount + "张票");
return false;
}
public static void main(String[] args) {
// 1.创建Ticket类的对象
Ticket ticket = new Ticket();
// 2.创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
// 3.启动线程
t1.start();
t2.start();
}
}
以下是同步静态方法
public class Ticket implements Runnable{
private static int ticketCount = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
if ("窗口1".equals(Thread.currentThread().getName())) {
boolean b = sale1();
if (b) break;
}
if ("窗口2".equals(Thread.currentThread().getName())) {
boolean b = sale1();
if (b) break;
}
}
}
public synchronized static boolean sale1(){
if (ticketCount <= 0) return true;
// 睡眠一会 因为卖票需要时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数减1
ticketCount--;
// 打印当前线程名称 和 剩余票数
String name = Thread.currentThread().getName();
System.out.println("当前线程名字:" + name + "卖了一张票,剩余:" + ticketCount + "张票");
return false;
}
public static void main(String[] args) {
// 1.创建Ticket类的对象
Ticket ticket = new Ticket();
// 2.创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
// 3.启动线程
t1.start();
t2.start();
}
}
3.lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁, 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作 Lock中提供了获得锁和释放锁的方法
1.void lock():获得锁
2.void unlock():释放锁 Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化 ReentrantLock的构造方法
3.ReentrantLock():创建一个ReentrantLock的实例
public class Ticket implements Runnable{
private int ticketCount = 100;
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 睡眠一会 因为卖票需要时间
try {
lock.lock();
if(ticketCount > 0) {
Thread.sleep(100);
// 票数减1
ticketCount--;
// 打印当前线程名称 和 剩余票数
String name = Thread.currentThread().getName();
System.out.println("当前线程名字:" + name + "卖了一张票,剩余:" + ticketCount + "张票");
}else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 放在finally中释放锁 防止出异常 导致锁无法释放
lock.unlock();
}
}
}
public static void main(String[] args) {
// 1.创建Ticket类的对象
Ticket ticket = new Ticket();
// 2.创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
// 3.启动线程
t1.start();
t2.start();
t3.start();
}
}
4.死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
出现的条件就是锁嵌套 线程一 会一直等待obj2锁对象 线程二 会一直等待obj1锁对象
public class Ticket {
public static void main(String[] args) {
Object obj1 = new Object();// 锁对象1
Object obj2 = new Object();// 锁对象2
new Thread(() -> {
synchronized (obj1){
System.out.println(Thread.currentThread().getName() + "拿到了obj1锁对象");
synchronized (obj2){
System.out.println(Thread.currentThread().getName() + "拿到了obj2锁对象");
}
}
}, "窗口1").start();
new Thread(() -> {
synchronized (obj2){
System.out.println(Thread.currentThread().getName() + "拿到了obj2锁对象");
synchronized (obj1){
System.out.println(Thread.currentThread().getName() + "拿到了obj1锁对象");
}
}
}, "窗口2").start();
}
}
5.等待唤醒机制(wait、notify、notifyAll、阻塞队列)
等待和唤醒的方法
为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中 Object类的等待和唤醒方法:
阻塞队列实现等待唤醒机制
BlockingQueue的核心方法:
put(anObject):将参数放入队列,如果放不进去会阻塞。
take():取出第一个数据,取不到会阻塞。
常见BlockingQueue:
ArrayBlockingQueue:底层是数组,有界。
LinkedBlockingQueue:底层是链表,无界。但不是真正的无界,最大为int的最大值。
案例:吃货吃汉堡 厨师做汉堡 做一个吃一个
public class Cooker extends Thread {
private ArrayBlockingQueue<String> bd;
public Cooker(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}
// 生产者步骤:
// 1,判断桌子上是否有汉堡包
// 如果有就等待,如果没有才生产。
// 2,把汉堡包放在桌子上。
// 3,叫醒等待的消费者开吃。
@Override
public void run() {
while (true) {
try {
bd.put("汉堡包");
System.out.println("厨师放入一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Foodie extends Thread {
private ArrayBlockingQueue<String> bd;
public Foodie(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}
@Override
public void run() {
// 1,判断桌子上是否有汉堡包。
// 2,如果没有就等待。
// 3,如果有就开吃
// 4,吃完之后,桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
// 汉堡包的总数量减一
//套路:
//1. while(true)死循环
//2. synchronized 锁,锁对象要唯一
//3. 判断,共享数据是否结束. 结束
//4. 判断,共享数据是否结束. 没有结束
while (true) {
try {
String take = bd.take();
System.out.println("吃货将" + take + "拿出来吃了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) {
ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);
Foodie f = new Foodie(bd);
Cooker c = new Cooker(bd);
f.start();
c.start();
}
}
6.线程的状态
虚拟机的6种状态总结
7.线程池 (创建线程池、线程池的拒绝策略)
1.Executors静态方法
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
//1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
ExecutorService executorService = Executors.newCachedThreadPool();
//Executors --- 可以帮助我们创建线程池对象
//ExecutorService --- 可以帮助我们控制线程池
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
//Thread.sleep(2000);
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
executorService.shutdown();
}
}
public class MyThreadPoolDemo2 {
public static void main(String[] args) {
//参数不是初始值而是最大值
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
System.out.println(pool.getPoolSize());//0
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
System.out.println(pool.getPoolSize());//2
// executorService.shutdown();
}
}
2.创建线程池对象
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
核心线程数量 不会被清除
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "在执行了");
}
}
public class MyThreadPoolDemo3 {
// 参数一:核心线程数量
// 参数二:最大线程数
// 参数三:空闲线程最大存活时间
// 参数四:时间单位
// 参数五:任务队列
// 参数六:创建线程工厂
// 参数七:任务的拒绝策略
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.shutdown();
}
}
拒绝策略
1.ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。就是超过最大数量线程执行后的任务直接抛弃并抛异常
2.ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。 3.ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。就是只能有1个任务在等待 后来的任务只会替换掉正在等待的任务
4.ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。就是超过最大的任务后,再来新任务不会等待而是直接运行(new Thread)
8.volatile内存可见性
1,堆内存是唯一的,每一个线程都有自己的线程栈。
2 ,每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。
3 ,在线程中,每一次使用是从变量的副本中获取的。
问题
如果A线程修改了堆中共享变量的值。 那么其他线程不一定能及时使用最新的值。
volatile关键字可以强制线程每次在使用的时候,都会看一下共享区域最新的值
Synchronized同步代码块 也可以实现
1 ,线程获得锁
2 ,清空变量副本
3 ,拷贝共享变量最新的值到变量副本中
4 ,执行代码
5 ,将修改后变量副本中的值赋值给共享数据
6 ,释放锁
9.原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断, 要么所有的操作都不执行,多个操作是一个不可以分割的整体。
volatile关键字只能保证内存可见性,无法保证原子性,当多个线程同时操作共享变量时volatile关键字只能保证操作前的值时最新的,但是多个线程同时修改后,同步到内存中,值就不是最新的了
10.原子类
原子类AtomicInteger
public AtomicInteger(): 初始化一个默认值为0的原子型
Integer public AtomicInteger(int initialValue): 初始化一个指定值的原子型
Integer int get(): 获取值
int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
AtomicInteger原理
自旋锁 + CAS 算法
CAS算法:
1.有3个操作数(内存值V, 旧的预期值A,要修改的值B)
2.当旧的预期值A == 内存值 此时修改成功,将V改为B 当旧的预期值A!=内存值 此时修改失败,不做任何操作
3.并重新获取现在的最新值(这个重新获取的动作就是自旋)
大白话:
在修改共享数据的时候,把原来的旧值记录下来了。
如果现在内存中的值跟原来的旧值一样,证明没有其他线程操作过内存值,则修改成功。
如果现在内存中的值跟原来的旧值不一样了,证明已经有其他线程操作过内存值了。
则修改失败,需要获取现在最新的值,再次进行操作,这个重新获取就是自旋。
synchronized和CAS的区别
相同点:在多线程情况下,都可以保证共享数据的安全性。
不同点: synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁) cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数 据的时候,会检查一下,别人有没有修改过这个数据。 如果别人修改过,那么我再次获取现在最新的值。 如果别人没有修改过,那么我现在直接修改共享数据的值。 (乐观锁)
11.并发工具类(Hashtable、ConcurrentHashMap、CountDownLatch、Semaphore)
Hashtable
HashMap是线程不安全的(多线程环境下可能会存在问题)。 为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
1 ,Hashtable采取悲观锁synchronized的形式保证数据的安全性
2 ,只要有线程访问, 会将整张表全部锁起来, 所以Hashtable的效率低下
ConcurrentHashMap
1 ,HashMap是线程不安全的。多线程环境下会有数据安全问题
2 ,Hashtable是线程安全的,但是会将整张表锁起来,效率低下
3,ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样
jdk7
1,根据键的哈希值计算出Segment数组的索引。(Segment数组不会扩容 一旦创建 不可修改长度 扩容因子是扩容里面的小数组用的)
2,如果为空,就创建一个长度默认为2的 并把该数组的索引值存放到Segment数组 元素超过扩容因子 就会扩容到原来的两倍
3,再次利用键的哈希值计算出在小数组应存入的索引(二次哈希)
4,如果为空,则直接添加。
总结:
创建 ConcurrentHashMap时:
1.默认创建一个长度16,加载因子为0.75的大数组。这个大数组一旦创建无法扩容
2.还会创建一个长度为2的小数组,把地址值复制给0索引处,其他索引位置的元素都是null
向ConcurrentHashMap存放数据时:
1.第一次会根据键的哈希值来计算出在大数组中应存入的位置。如果为null,则按照模板创建小数组
创建完毕,会二次哈希,计算出在小数组中应存入的位置。直接存入
2.如果不为null,就会根据记录的地址值找到小数组。二次哈希,计算出在小数组中应存入的位置。如果需要扩容,则将小数组扩容两倍,如果不需要扩容,则就会看小数据的这个位置有没有元素,如果没有则直接存,如果有元素,就会调用equals方法,比较属性值 如果equals为true,则不存, 如果equals为false,形成哈希桶结构
jdk8
底层结构:哈希表。(数组、链表、红黑树的结合体)。 结合CAS机制 + synchronized同步代码块形式保证线程安全。
总结
1,如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表
2,计算当前元素应存入的索引。
3,如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
4,如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在 他下面,变成链表。
5,当链表的长度大于等于8时,自动转换成红黑树
6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性。
CountDownLatch
使用场景: 让某一条线程等待其他线程执行完毕之后再执行。
CountDownLatch(int count):参数写等待线程的数量。并定义了一个计数器。
await():让线程等待,当计数器为0时,会唤醒等待的线程
countDown(): 线程执行完毕时调用,会将计数器-1。
代码如下
public class ChildThread1 extends Thread {
private CountDownLatch countDownLatch;
public ChildThread1(CountDownLatch cl) {
this.countDownLatch = cl;
setName("小明");
}
@Override
public void run() {
// 1. 吃饺子
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 吃饺子。。。" + (i + 1));
}
// 2. 告诉妈妈吃完了
countDownLatch.countDown();
}
}
public class ChildThread2 extends Thread {
private CountDownLatch countDownLatch;
public ChildThread2(CountDownLatch cl) {
this.countDownLatch = cl;
setName("小红");
}
@Override
public void run() {
// 1. 吃饺子
for (int i = 0; i < 12; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 吃饺子。。。" + (i + 1));
}
// 2. 告诉妈妈吃完了
countDownLatch.countDown();
}
}
public class ChildThread3 extends Thread {
private CountDownLatch countDownLatch;
public ChildThread3(CountDownLatch cl) {
this.countDownLatch = cl;
setName("小刚");
}
@Override
public void run() {
// 1. 吃饺子
for (int i = 0; i < 15; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 吃饺子。。。" + (i + 1));
}
// 2. 告诉妈妈吃完了
countDownLatch.countDown();
}
}
public class MatherThread extends Thread {
private CountDownLatch countDownLatch;
public MatherThread(CountDownLatch cl) {
this.countDownLatch = cl;
setName("妈妈");
}
@Override
public void run() {
//1.等待其他线程结束
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.执行业务
System.out.println(Thread.currentThread().getName()+" 收拾碗筷。。。");
}
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);//有三个孩子
MatherThread matherThread = new MatherThread(countDownLatch);
matherThread.start();
ChildThread1 t1 = new ChildThread1(countDownLatch);
ChildThread2 t2 = new ChildThread2(countDownLatch);
ChildThread3 t3 = new ChildThread3(countDownLatch);
t1.start();
t2.start();
t3.start();
}
}
Semaphore
使用场景: 可以控制访问特定资源的线程数量。
步骤:
1,需要有人管理这个通道 --> 创建Semaphore对象
2,当有车进来了,发通行许可证 --> acquire()发通行证
3,当车出去了,收回通行许可证 --> release()收回通行证
4,如果通行许可证发完了,那么其他车辆只能等着
代码:
public class MyRunnable implements Runnable{
private Semaphore semaphore = new Semaphore(2);// 同一时间只能有两个线程 执行
@Override
public void run() {
//1.获取通行证 如果获取不到 会一直等待 直到获取到为止
try {
semaphore.acquire();
// 2.执行业务
System.out.println(Thread.currentThread().getName()+" 获取了通行证。。。正在通行");
Thread.sleep(2000);
// 3.释放通行证
System.out.println(Thread.currentThread().getName()+" 释放了通行证。。。");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < 10; i++) {
new Thread(myRunnable).start();
}
}
}