四十四、线程安全
线程安全
1、问题:
【测试类】
public class TicketDemo {
public static void main(String[] args) {
// 创建Runnable实现类对象
Ticket ticket = new Ticket();
// 创建三个线程类对象
Thread t1 = new Thread(ticket, "窗口1:");
Thread t2 = new Thread(ticket, "窗口2:");
Thread t3 = new Thread(ticket, "窗口3:");
// 开启线程
t1.start();
t2.start();
t3.start();
}
}
【线程类】
// 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:
public class Ticket implements Runnable {
private int ticketCount = 100;// 三个窗口共同操作的一百张票
@Override
public void run() {
while (true) {// 三个窗口需要把100张票面光
// 判断票数大于0,就卖票,并告知是哪个窗口卖的
if (ticketCount <= 0) {
break;// 停止死循环
} else {
// 模拟卖票所花费的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 还有剩余票数 , 可以继续卖票
ticketCount--;
System.out.println(Thread.currentThread().getName() + "卖了一张票, 还剩" + ticketCount + "张");
}
}
}
}
以上代码售票出现的问题:
- 相同的票出现了多次
- 出现了负数的票
问题原因:
多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了!!!
2、解决
为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)
- 多线程操作共享数据
如何解决多线程安全问题?
- 基本思想:让共享数据存在安全的环境中,在某一个线程访问共享数据时 其他线程是无法操作的
怎么实现呢?
- 把多线程操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java 提供了同步代码块的方式来解决
3、线程的同步
Java 允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突。因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。
三种实现同步的方式:
-
同步代码块
-
同步方法
-
锁机制。Lock
3.1 同步代码块
锁住多条语句操作共享数据,可以使用同步代码块实现
格式:
synchronized(任意对象){
多条语句
}
注意:
- 默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
- 当线程执行完出来了,锁才会自动打开
- 锁对象可以是任意对象,但是多个线程必须使用同一把锁。
优缺点:
好处:解决了多线程的数据安全问题
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
代码体现:
【测试类】
public class TicketDemo {
public static void main(String[] args) {
// 创建任务类对象
Ticket ticket = new Ticket();
// 创建三个线程类对象
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
// 开启三个线程
t1.start();
t2.start();
t3.start();
}
}
【线程类】
public class Ticket implements Runnable {
private int ticketCount = 100; // 一共有一百张票
Object lock = new Object();
@Override
public void run() {// t1 , t2 , t3
while (true) {
synchronized (lock) { // 获取锁
// 如果票的数量为0 , 那么停止买票
if (ticketCount <= 0) {
break;
} else {
// 有剩余的票 , 开始卖票
ticketCount--;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张");
}
}// 释放锁
// 模拟出票的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.2 同步方法
就是把 synchronized 关键字加到方法上,保证线程执行该方法的时候,其他线程只能在方法外等着
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){
代码...
}
与同步代码块区别:
- 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
注意: 同步方法时不能指定锁对象的,但是有默认存在的锁对象的。
-
对于非static方法,同步锁就是this。
【测试类】 public class TicketDemo { public static void main(String[] args) { // 创建任务类对象 Ticket ticket = new Ticket(); // 创建三个线程类对象 Thread t1 = new Thread(ticket,"窗口1"); Thread t2 = new Thread(ticket,"窗口2"); Thread t3 = new Thread(ticket,"窗口3"); // 开启三个线程 t1.start(); t2.start(); t3.start(); } } 【线程类】 public class Ticket implements Runnable { private int ticketCount = 100; // 一共有一百张票 @Override public void run() { while (true) {// 死循环 // t1 , t2 , t3 if ( method() ) { break; } } } // 非静态方法 : 默认锁对象this private synchronized boolean method() { // 如果票的数量为0 , 那么停止买票 if (ticketCount <= 0) { return true; } else { // 模拟出票的时间 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 有剩余的票 , 开始卖票 ticketCount--; System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张"); } return false; } } -
对于static 方法,我们使用当前方法所在类的字节码对象(类名.calss)。Class类型的对象
【测试类】 public class TicketDemo { public static void main(String[] args) { // 创建任务类对象 Ticket ticket1 = new Ticket(); Ticket ticket2 = new Ticket(); Ticket ticket3 = new Ticket(); // 创建三个线程类对象 Thread t1 = new Thread(ticket1,"窗口1"); Thread t2 = new Thread(ticket2,"窗口2"); Thread t3 = new Thread(ticket3,"窗口3"); // 开启三个线程 t1.start(); t2.start(); t3.start(); } } 【线程类】 public class Ticket implements Runnable { private static int ticketCount = 100; // 一共有一百张票 @Override public void run() { while (true) {// 死循环 // t1 , t2 , t3 if ( method() ) { break; } } } // 静态方法 : 默认锁对象,当前类的字节码对象 // 同一个类的字节码对象一定是相同的 private static synchronized boolean method() { // 如果票的数量为0 , 那么停止买票 if (ticketCount <= 0) { return true; } else { // 模拟出票的时间 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 有剩余的票 , 开始卖票 ticketCount--; System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张"); } return false; } }
3.3 Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock中提供了获得锁和释放锁的方法:
- void lock():获得锁
- void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法:
- ReentrantLock():创建一个ReentrantLock的实例
注意:
多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用
步骤:
- 创建Lock锁对象
- 调用 lock() 方法获得锁
- 调用 umlock() 释放锁
【测试类】
public class TicketDemo {
public static void main(String[] args) {
// 创建任务类对象
Ticket ticket = new Ticket();
// 创建三个线程类对象
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
// 开启三个线程
t1.start();
t2.start();
t3.start();
}
}
【线程类】
public class Ticket implements Runnable {
private int ticketCount = 100; // 一共有一百张票
// 创建锁对象
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();// 加锁
// 如果票的数量为0 , 那么停止买票
if (ticketCount <= 0) {
rl.unlock(); //很重要
// 如果没有添加这句代码,第一线程进来,没有解锁,就跳出去了。下一个线程也进不来 。所以程序无法执行下去。
break;
} else {
// 有剩余的票 , 开始卖票
ticketCount--;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张");
}
// 释放锁
lock.unlock();
// 模拟出票的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4、死锁
4.1 概述
死锁一一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会有概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的。
4.2 死锁产生的条件分析
-
多个线程
-
存在锁对象的循环依赖
线程一中
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");
}
}
代码体现:
public class DeadLockDemo {
public static void main(String[] args) {
String 筷子A = "筷子A";
String 筷子B = "筷子B";
new Thread(
new Runnable() {
@Override
public void run() {
while (true) {
synchronized (筷子A) {
System.out.println("小明拿到了筷子A , 等待筷子B...");
synchronized (筷子B) {
System.out.println("小明拿到了筷子A和筷子B , 开吃!");
}// 释放筷子B
}// 释放筷子A
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
, "小明").start();
new Thread(
new Runnable() {
@Override
public void run() {
while (true) {
synchronized (筷子B) {
System.out.println("小红拿到了筷子B , 等待筷子A...");
synchronized (筷子A) {
System.out.println("小红拿到了筷子A和筷子B , 开吃!");
}// 释放筷子A
}// 释放筷子B
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
, "小红").start();
}
}
4.3 如何避免死锁
避免使用锁的嵌套。
5、线程的状态
在java.lang.Thread.state这个枚举中给出6种状态:
新建状态(NEW) -----------> 创建线程对象
就绪状态(RUNNABLE) -----------> start方法
阻塞状态(BLOCKED) -----------> 无法获取锁对象
等待状态(WAITING) -----------> wait方法
计时等待(TIMED_WAITING)----------> sleep方法
结束状态(TERMINATED) -----------> 全部代码运行完毕

6、线程间的通讯
6.1 概述
线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法,如下:
等待方法:
void wait() 让线程进入无限等待。
void wait(long timeout) 让线程进入计时等待
以上两个方法调用会导致当前线程释放掉锁资源。
唤醒方法:
void notify() 唤醒在此对象监视器(锁对象)上等待的单个线程。
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
以上两个方法调用不会导致当前线程释放掉锁资源。
6.2 注意
- 等待和唤醒方法,都是使用锁对象调用(需要在同步代码块中使用)
- 等待和唤醒方法应该使用相同的锁对象调用。
- 等待的方法会释放锁,唤醒的方法不会释放锁
6.3 代码使用
这两种方法都是Object的方法
wait(): 必须要用锁对象来调用;会释放锁
public static void main(String[] args) {
Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("A线程开始执行");
System.out.println("A线程进入无线等待状态...");
try {
// wait方法必须拿锁对象进行调用 , 负责会出现异常 : 监视器非法状态异常(IllegalMonitorStateException)
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A线程执行完毕...");
}
}
}, "A").start();
}
void wait(long timeout) : 让线程进入计时等待 , timeout代表long类型毫秒值
public class Test3 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (Test3.class) {// Test3.class获取此类的字节码对象
System.out.println("线程开始执行...");
System.out.println("线程进入到及时等待状态...");
try {
Test3.class.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程执行完毕..");
}
}).start();
}
}
notify(): 必须要用锁对象来调用;不会释放锁,继续往下执行当前线程
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {// 获得锁
System.out.println("A线程开始执行");
System.out.println("A线程进入无线等待状态...");
try {
// wait方法必须拿锁对象进行调用 , 负责会出现异常 : 监视器非法状态异常(IllegalMonitorStateException)
lock.wait();// 释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A线程执行完毕...");// 只有A线程被唤醒, 才可以执行到此位置代码!
}
}
}, "A").start();
// 睡眠一秒钟 , 为了让A线程先执行
Thread.sleep(5000);
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {// 获得锁
System.out.println("B线程开始执行");
System.out.println("B线程唤醒此监视器上随机一个等待线程...");
// 唤醒lock锁上随机一个等待的线程
lock.notify();// 没有释放锁
// lock.notifyAll();// 唤醒lock锁所有等待的线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}// 释放锁!
}
}, "B").start();
}
7、线程池
7.1 概述
线程使用存在的问题
- 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
- 如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源。
线程池的认识
其实就是一个容量多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
线程池使用大致流程
- 创建线程池指定线程开启的数量
- 提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
- 线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
- 如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任务
好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建 , 就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存 , 服务器死机 (每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
7.2 线程池API的学习
java.util.concurrent.ExecutorService 是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了。
获取线程池我们使用工具类 java.util.concurrent.Executors的静态方法:
public static ExecutorService newFixedThreadPool(int num) 指定线程池最大线程池数量获取线程池
线程池ExecutorService的相关方法:
提交执行任务方法:
<T> Future<T> submit(Callable<T> task)
Future<?> submit(Runnable task)
关闭线程池方法(一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭)
void shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
代码体现:
教练教学生游泳
【测试类】
public static void main(String[] args) {
// 创建线程池指定3个线程
ExecutorService pool = Executors.newFixedThreadPool(3);// 创建一个三个线程的线程池
Student s1 = new Student("小花");
Student s2 = new Student("小明");
Student s3 = new Student("小红");
Student s4 = new Student("小刚");
Student s5 = new Student("小强");
// 给线程池提交任务
pool.submit(s1);
pool.submit(s2);
pool.submit(s3);
pool.submit(s4);
pool.submit(s5);
// 关闭线程池
// pool.shutdown();
}
【学生类】
class Student implements Runnable {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在教" + name + "学习游泳...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "教" + name + "学习游泳完毕!");
}
}
7.3 callable跟线程池使用
Callable任务处理使用步骤:
- 创建线程池
- 定义Callable任务
- 创建Callable任务,提交任务给线程池
- 获取执行结果
<T> Future<T> submit(Callable<T> task) 提交Callable任务方法
返回值类型Future的作用就是为了获取任务执行的结果。Future是一个接口,里面存在一个get方法用来获取值。
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(mc);
Future<Integer> future1 = pool.submit(mc);
Future<Integer> future2 = pool.submit(mc);
// Integer sum = future.get();
// System.out.println(sum);
// pool.shutdown();
}
}
// 使用线程池计算 从0~n的和,并将结果返回
public class MyCallable implements Callable<Integer> {
private int num = 0;
@Override
public Integer call() throws Exception {
while (true) {
synchronized (MyCallable.class){
if(num >=100){
break;
}else{
num += 1;
System.out.println(Thread.currentThread().getName()+":"+num);
}
}
Thread.sleep(100);
}
return num;
}
}
8、自定义线程池
ThreadPoolExecutor类 : Java提供好的类
public ThreadPoolExecutor(
int corePoolSize, 核心线程数量
int maximumPoolSize, 最大线程数量
long keepAliveTime, 线程临时存活时间
TimeUnit unit, 线程临时存活时间单位
BlockingQueue<Runnable> workQueue, 阻塞队列
ThreadFactory threadFactory, 创建线程方式
RejectedExecutionHandler handler) 任务的拒绝策略
核心线程数量 : 3 核心线程数量为3
最大线程数量 : 10 线程中最多线程数量为10
线程临时存活时间 : 60 临时线程多长时间没有任务销毁线程的时间
线程临时存活时间单位 : 分钟|秒|消失
阻塞队列 : 长度10 集合
创建线程方式 ;
任务的拒绝策略 : 可选
使用:
public static void main(String[] args) {
// 使用Java提供好的线程池
// Executors.newFixedThreadPool(10);
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, // 核心线程数量 : 3
10, // 最大线程数量 : 10
60, // 线程临时存活时间 : 60
TimeUnit.SECONDS, // 线程临时存活时间单位 : 分钟|秒|消失
new ArrayBlockingQueue<>(10),// 阻塞队列
Executors.defaultThreadFactory(),// 创建线程方式
new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
);
for (int i = 0; i < 30; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
});
}
}

浙公网安备 33010602011771号