线程安全
多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了
线程的同步
概述 : java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性
分类:
- 同步代码块
- 同步方法
- 锁机制。Lock
同步代码块
同步代码块 : 锁住多条语句操作共享数据,可以使用同步代码块实现
格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
注意:
- 默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
- 当线程执行完出来了,锁才会自动打开
- 锁对象可以是任意对象,但多个线程必须使用同一把锁
同步的好处和弊端:
- 好处 : 解决了多线程的数据安全问题
- 弊端 : 当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步方法
同步方法:就是把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数) { }
同步代码块和同步方法的区别:
- 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
注意 : 同步方法时不能指定锁对象的 , 但是有默认存在的锁对象的。
- 对于非static方法,同步锁就是this。
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。 Class类型的对象
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock中提供了获得锁和释放锁的方法:
- void lock():获得锁
- void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化:
- ReentrantLock的构造方法
- ReentrantLock():创建一个ReentrantLock的实例
注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用
线程死锁
概述 :
死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的
产生条件 :
- 多个线程
- 存在锁对象的循环依赖
package com.zqc.deadlock_demo;
/**
* 死锁 :
* 死锁是一种少见的,而且难于调试的错误,在两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。
* 我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的
*/
public class DeadLockDemo {
public static void main(String[] args) {
String 筷子左 = "筷子左";
String 筷子右 = "筷子右";
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (筷子左) {
System.out.println("张三获取了筷子左,等待筷子右...");
// 张三在这个位置....!!!!!!
synchronized (筷子右) {
System.out.println("张三获取了左筷子和筷子右...开吃");
}// 释放筷子右
}// 释放筷子左
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "张三").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (筷子右) {
System.out.println("小明获取了筷子右,等待筷子左...");
// 小明在这个位置.....!!!
synchronized (筷子左) {
System.out.println("小明获取了筷子右和筷子左...开吃");
}// 释放筷子左
}// 释放筷子右
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "小明").start();
}
}
线程的状态
虚拟机中线程的六种状态:
新建状态( NEW ) | 创建线程对象 |
就绪状态(RUNNABLE ) | start方法 |
阻塞状态(BLOCKED ) | 无法获得锁对象 |
等待状态(WAITING ) | wait方法 |
计时等待(TIMED_WAITING ) | sleep方法 |
结束状态(TERMINATED ) | 全部代码运行完毕 |
线程通信
- 线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例。
- 等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法,如下:
- 等待方法 :
- void wait() 让线程进入无限等待。
- void wait(long timeout) 让线程进入计时等待
- 以上两个方法调用会导致当前线程释放掉锁资源。
- 唤醒方法 :
- void notify() 唤醒在此对象监视器(锁对象)上等待的单个线程。
- void notifyAll() 唤醒在此对象监视器上等待的所有线程。
- 以上两个方法调用不会导致当前线程释放掉锁资源
- 注意:
- 等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中调用)
- 等待和唤醒方法应该使用相同的锁对象调用