文章目录
掌握内容:
- 造成线程不安全的问题原因
- 解决线程安全问题的方式:
- 同步代码块
- 同步方法
- Lock方法
- 掌握线程的通讯:wait()、notify()
- 线程池: Runnable、Callable
一、线程安全
1、线程安全问题:
-
条件:两个或多个线程
-
多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了。
-
此时:线程不安全
当多个线程只对共享数据进行读操作时,不会出现线程不安全的情况
2、解决线程安全
核心思想:把操作共享数据的代码,放到一个同步锁中执行,要让某个线程操作数据时,不要让其他线程使用。
1)同步代码块
因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。
"同步锁",把操作共享数据的代码包含起来,让其他线程排斥在外等待。
同步代码块中的锁是什么有什么要求:
是一个对象 可以是任意类型任意对象
多个线程需要使用同一个锁对象
-
格式:
-
synchronized(对象锁){ //操作共享数据的代码 }
-
-
示例:
-
public class Ticket implements Runnable{ //共享数据 int number=100; //对象锁 Object lock=new Object(); public void run(){ //同步代码块 synchronized(lock){ if(number>0){ System.out.println(线程名称+"正在卖出第"+number"号票"); number--; } } } }
-
2)同步方法
使用synchronized修饰的方法,就叫做同步方法,保证线程执行该方法的时候,其他线程只能在方法外等着(多个线程并发访问时只能互斥访问)
-
格式:
-
public synchronized 方法名(参数列表){ //操作共享数据的代码 }
-
同步方法上使用的同步锁是隐式的,分为两种:
- this锁 : 非静态方法
- 类.class锁 : 静态方法
-
-
示例:
-
public class Ticket implements Runnable{ //共享数据 int number=100; public void run(){ //调用同步方法 } //同步方法 public synchronized void method(){ if(number>0){ System.out.println(线程名称+"正在卖出第"+number"号票"); number--; } } } void main(String[] args){ //共享资源 Ticket tick = new Ticket();///线程任务 new Thread( tick , "窗口1" ).start(); new Thread( tick , "窗口2" ).start(); new Thread( tick , "窗口3" ).start(); }
-
3)Lock锁
1、java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有。
2、Lock是接口,其中的两个方法我们需要用到:
public void lock():加同步锁。
public void unlock():释放同步锁。
3、我们无需实现该接口,可以借助其子类ReentrantLock来实例化。
使用步骤:
1)创建Lock锁对象
2)调用lock()方法获取锁
3)调用unLock()释放锁
**注意:**多个线程使用相同的Lock锁对象,需要互斥访问的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用。
-
使用方式:
-
//多态的形式: 父引用 = 子类对象 Lock lock = new ReenTrantLock();
-
-
Lock中的常用方法:
- lock() 获取锁
- unlock() 释放锁
-
示例:
-
public class Ticket implements Runnable{ //共享数据 int number=100; //Lock锁 Lock lock = new ReenTrantLock(); public void run(){ lock.lock(); //获取锁 if(number>0){ System.out.println(线程名称+"正在卖出第"+number"号票"); number--; } lock.unlock();//释放锁 } }
-
二、线程死锁
死锁是一种少见的,而且难于调试的错误。
当两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。
- 死锁的现象
各进程、线程相互等待对方手里的资源,导致阻塞状态,程序无法向下运行。- 死锁的产生的原因
互斥条件
对必须互斥使用的资源的抢夺才会导致死锁
不剥夺条件
进程保持的资源只能主动释放,不可强行剥夺
请求和保持条件
保持着某些资源不放的同时,请求别的资源
循环等待条件
存在一种进程资源的循环等待,循环等待未必死锁,当不在循环内的进程释放了一些资源后,即可能解除循环等待现象。死锁一定有循环等待
-
在并发编程中,当使用嵌套的同步代码块时,会发生:死锁现象
-
示例:
//A锁 Object objA = new Object(); //B锁 Object objB = new Object() 线程一中 synchronized (objA) { System.out.println("嵌套1 objA"); synchronized (objB) { System.out.println("嵌套1 objB"); } } 线程二中 synchronized (objB) { System.out.println("嵌套2 objB"); synchronized (objA) { System.out.println("嵌套2 objA"); } }
-
-
避免死锁:
- 书写并发代码时,不使用嵌套的同步代码块
- 如果使用嵌套的同步代码块,不要在嵌套代码块中交替使用对象锁
三、线程的状态
- 在Thread类中,有一个枚举:State。 枚举中有6种线程状态
-
创建
- 当创建Thread对象时,进行new状态
-
可运行
- 当调用start()方法时,进入run状态
-
锁阻塞
- 当没有获取到锁对象时,进入锁阻塞状态
-
无限等待
- 当调用wait()方法时,进入到无限等待状态
- 如果没有其他线程唤醒处理等待状态的线程,等待状态的线程会一直处于等待
- 使用notify()或notifyAll()唤醒
- 当调用wait()方法时,进入到无限等待状态
-
计时等待
- 当调用:sleep(毫秒)、wait(毫秒)方法时,进入计时等待状态
- 时间超过时,会进行到"run状态"或"锁阻塞"
- 当调用:sleep(毫秒)、wait(毫秒)方法时,进入计时等待状态
-
死亡(终止)
- 当线程执行完后,进行到死亡状态
-
四、线程的通讯
1、什么是线程间通讯
线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例
本质:就是使用wait()和notify()实现两个不同线程之间的通讯
若A线程进入等待状态,可通过B线程通过唤醒,让A线程进入可运行状态
2、等待和唤醒的方法调用注意事项
1)等待方法
void wait() 让线程进入无限等待。
void wait(long timeout) 让线程进入计时等待
以上两个方法调用会导致当前线程释放掉锁资源。
2)唤醒方法
void notify() 唤醒在此对象监视器(锁对象)上等待的(任意)单个线程。
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
以上两个方法调用不会导致当前线程释放掉锁资源。
注意:
1.等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中调用)。
2.等待和唤醒方法应该使用相同的锁对象调用。
Object lock = new Object();
synchronized(lock){
lock.wait();//当前获取锁的线程,进入无限等待状态 (释放锁)
}
其他线程:
lock.notify();//唤醒使用lock锁对象上的任意一个处于等待状态的线程
lock.notifyAll();//唤醒使用lock锁对象上的所有的线程
3、生产者消费者案例
定义一个集合当做盘子,包子铺线程完成生产包子,包子添加到集合中;吃货线程完成购买包子,包子从集合中移除。
当包子没有时,吃货线程等待
包子铺线程生产包子,并通知吃货线程去吃包子
当盘子装满(100)时,包子铺线程等待
4.吃货线程去吃包子,并通知包子铺去做包子
import java.util.ArrayList;
// 生产者
class Producer extends Thread {
// 共享资源
ArrayList<String> list = null;
public Producer(String name, ArrayList<String> list) {
super(name);
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) { // 对象锁可以是任意类型的
String name = Thread.currentThread().getName();
if (list.isEmpty()) {
// 生产包子
System.out.println(name + ": 生产包子");
list.add("包子");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒 顾客一 吃包子
System.out.println(name + ": 叫 吃货吃包子");
list.notify();
} else {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
// 消费者
class Consumer extends Thread {
// 共享资源
ArrayList<String> list = null;
public Consumer(String name, ArrayList<String> list) {
super(name);
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
String name = Thread.currentThread().getName();
if (list.isEmpty()) { // 没有包子
// 唤醒生产者 --> 生产包子
System.out.println("\t\t"+name + ": 叫包子铺生产包子");
list.notify(); // 唤醒时不会释放锁
// 进入等待 --> 释放锁了
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("\t\t"+name + " 吃包子");
list.remove(0); // 吃包子
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class Demo05ProducerConsumer {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
new Producer("包子铺", list).start();
new Consumer("吃货", list).start();
}
}
五、线程池
1、 线程使用存在的问题
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源。
2 、线程池的认识
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
java.util.concurrent.ExecutorService 是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了。
获取线程池我们使用工具类java.util.concurrent.Executors的静态方:
public static ExecutorService newFixedThreadPool (int num) 指定线程池最大线程池数量获取线程池
3、线程池使用大致流程
创建线程池指定线程开启的数量
提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任务。
4、请简述线程池使用的好处
降低资源消耗。
提高响应速度。
提高线程的可管理性。
5、线程池处理Runnable任务
1、Runnable接口使用步骤
- 指定线程数量获取线程池
- 定义Runnable任务类型
- 创建任务对象,提交给线程池
2、Runnable实现类代码
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 2. 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 3. 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}
6、线程池处理Callable任务
1 、Callable接口概述
public interface Callable<V> {
V call() throws Exception;
}
------------------------------------
public interface Runnable{
void run();
}
**Callable与Runnable的不同点:**
1. Callable支持结果返回,Runnable不行
2. Callable可以 抛出异常,Runnable不行
2、 Callable任务处理使用步骤
-
创建线程池对象,并指定线程池中Thread的数量
-
//线程池:ExecutorService (接口类型) //实例化线程池: ExecutorService es = Executors.newFixedThreadPool(10);//线程池中有10个Thread
-
-
把线程任务,提交给线程池(线程池会分配一个Thread对象,来执行当前的任务),当Thread执行完任务之后,会回归到线程池中
-
// <T> Future<T> submit(Callable<T> task) 提交Callable任务方法 //方法: submit(Runnable task) Future<Integer> f = es.submit(task);
-
-
获取执行结果
-
// 返回值类型Future的作用就是为了获取任务执行的结果。 // Future是一个接口,里面存在一个get方法用来获取值。 Integer integer = f.get();
-
代码实现:
// 计算1-100之间的和
package cn.itcast.day10.demo01Lambda;
import java.util.concurrent.*;
class Task implements Callable<Integer>{
private int n;
public Task(int n){
this.n = n;
}
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
System.out.println("任务完成--》我计算完了!回到线程池...");
return sum;
}
}
public class Demo02{
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.创建线程池 有五个线程
ExecutorService es = Executors.newFixedThreadPool(2);
// 2.定义callable任务
Task task = new Task(5);
Task task2 = new Task(10);
Task task3 = new Task(100);
// 3.把callable任务--》提交给线程池
Future<Integer> f = es.submit(task);
Future<Integer> f2 = es.submit(task2);
Future<Integer> f3 = es.submit(task3);
// 4.获取执行结果
Integer integer = f.get();
Integer integer2 = f2.get();
Integer integer3 = f3.get();
System.out.println("输出结果:");
System.out.println(integer);
System.out.println(integer2);
System.out.println(integer3);
es.shutdown();
}
}
面试题 :java线程的创建有几种方式?
- Thread
- Runnable
- Callable (必须使用线程池来实现)