一、线程安全问题
- 1.什么是线程安全问题:
- 当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题;但是做读操作是不会发生数据冲突问题
- 2.线程安全解决办法:
- a)内置锁(synchronized)、b)显示锁(lock锁)
- 3.为什么使用线程同步或使用锁能解决线程安全问题:
- 将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
- 4.什么是多线程同步:
- 当多个线程共享同一个资源,不会受到其他线程的干扰
- 5.买票案例演示线程安全问题
- 共享的变量count代表总共有100张票
- 线程t1和t2同时对count执行–操作
- 从打印结果我们可以看出,售票出现了异常(也就是多线程安全问题)
class SaleTicket implements Runnable {
private int count = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
sale();
}
}
private void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " -> 当前售票:" + (100 - count + 1));
}
--count;
}
}
public class App {
public static void main(String[] args) {
SaleTicket saleTicket = new SaleTicket();
Thread t1 = new Thread(saleTicket,"t1线程");
Thread t2 = new Thread(saleTicket,"t2线程");
t1.start();
t2.start();
}
}
二、内置锁(synchronized)
- 1.Java的内置锁机制
- Java提供了一种内置的锁机制来支持原子性;每一个Java对象都可以用作一个实现同步的锁,称为内置锁,线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁
- 2.什么是互斥锁
- 即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁;
- 内置锁也是互斥锁,但是它会降低程序的效率
- 3.synchronized的两种实现方法
- a)同步方法
- b)同步代码块
三、同步代码块
- 1.相同Runnable对象的同步实现
同步代码块的锁要使用同一个对象
,如代码中的private Object oj = new Object();
class SaleTicket implements Runnable {
private int count = 100;
private Object oj = new Object();
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
sale();
}
}
private void sale() {
synchronized (oj) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " -> 当前售票:" + (100 - count + 1));
}
--count;
}
}
}
public class App {
public static void main(String[] args) {
SaleTicket saleTicket = new SaleTicket();
Thread t1 = new Thread(saleTicket, "t1线程");
Thread t2 = new Thread(saleTicket, "t2线程");
t1.start();
t2.start();
}
}
- 2.不同Runnable对象的同步实现
- t1和t2线程使用的是不同的Runnable对象
private static int count = 100;
:为了保证操作是同一个count,加上了static关键字private static Object oj = new Object();
:为了保证使用的是同一个锁对象,加上了static关键字
class SaleTicket implements Runnable {
private static int count = 100;
private static Object oj = new Object();
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
sale();
}
}
private void sale() {
synchronized (oj) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " -> 当前售票:" + (100 - count + 1));
}
--count;
}
}
}
public class App {
public static void main(String[] args) {
SaleTicket saleTicket = new SaleTicket();
SaleTicket saleTicket2 = new SaleTicket();
Thread t1 = new Thread(saleTicket, "t1线程");
Thread t2 = new Thread(saleTicket2, "t2线程");
t1.start();
t2.start();
}
}
四、同步方法
- 1.非静态同步方法
- 非静态同步方法使用的是this锁,相当于
synchronized (this)
- 非静态同步方法使用的是this锁,相当于
class SaleTicket implements Runnable {
private int count = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
sale();
}
}
private synchronized void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " -> 当前售票:" + (100 - count + 1));
}
--count;
}
}
public class App {
public static void main(String[] args) {
SaleTicket saleTicket = new SaleTicket();
Thread t1 = new Thread(saleTicket, "t1线程");
Thread t2 = new Thread(saleTicket, "t2线程");
t1.start();
t2.start();
}
}
- 2.静态同步方法
- 因为使用的是静态方法,所以count也必须修改为静态对象
private static int count = 100;
- 使用当前class字节码文件,相当于
synchronized (ThreadDemo01.class)
- 因为使用的是静态方法,所以count也必须修改为静态对象
class SaleTicket implements Runnable {
private static int count = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
sale();
}
}
private static synchronized void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " -> 当前售票:" + (100 - count + 1));
}
--count;
}
}
public class App {
public static void main(String[] args) {
SaleTicket saleTicket = new SaleTicket();
Thread t1 = new Thread(saleTicket, "t1线程");
Thread t2 = new Thread(saleTicket, "t2线程");
t1.start();
t2.start();
}
}
五、多线程死锁
- 1.什么是多线程死锁
- 同步中嵌套同步,导致锁无法释放
- 2.死锁的实例分析
- 增加了一个flag,这个主要是用来控制t1和t2分别执行不同的逻辑
- 对t1来说,flag=true,先获取oj锁,再获取this锁
- 对t2来说,flag=false,先获取this锁,再获取oj锁
- 在某一时刻t1获得了oj锁,没有获取到this锁;而t2获取了this锁,没有获取到oj锁,从而导致了死锁
class SaleTicket implements Runnable {
private int count = 100;
private Object oj = new Object();
public boolean flag = true;
@Override
public void run() {
if(flag){
while (count > 0){
synchronized (oj){
sale();
}
}
}else {
while (count > 0){
sale();
}
}
}
private synchronized void sale() {
synchronized (oj) {
try {
Thread.sleep(10);
}catch (Exception e){
}
if (count > 0) {
System.out.println(Thread.currentThread().getName() + " -> 当前售票:" + (100 - count + 1));
}
--count;
}
}
}
public class App {
public static void main(String[] args) throws InterruptedException {
SaleTicket saleTicket = new SaleTicket();
Thread t1 = new Thread(saleTicket, "t1线程");
Thread t2 = new Thread(saleTicket, "t2线程");
t1.start();
Thread.sleep(40);
saleTicket.flag = false;
t2.start();
}
}
六、Threadlocal
- 1.什么是Threadlocal
- ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量
- 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
- 2.ThreadLocal接口
- a)
void set(Object value)
:设置当前线程的线程局部变量的值 - b)
public Object get()
:返回当前线程所对应的线程局部变量 - c)
public void remove()
:将当前线程局部变量的值删除,目的是为了减少内存的占用(这个不是必要的,因为线程结束后会自动回收,这个只是加快内存回收的速度) - d)
protected Object initialValue()
:回该线程局部变量的初始值,这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次
- a)
- 3.Threadlocal使用
class Res {
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public Integer getNumber() {
int count = threadLocal.get() + 1;
threadLocal.set(count);
return count;
}
}
class MyThread extends Thread {
private Res res;
public MyThread(Res res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + this.res.getNumber());
}
}
}
public class App {
public static void main(String[] args) {
Res res = new Res();
MyThread t1 = new MyThread(res);
MyThread t2 = new MyThread(res);
t1.start();
t2.start();
}
}
七、多线程三大特性
- 1.原子性
- 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 一个很经典的例子就是银行账户转账问题:
- 比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
- 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 2.可见性
- 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题
- 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 3.有序性
- 程序执行的顺序按照代码的先后顺序执行
- 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化
- 它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
- 程序执行的顺序按照代码的先后顺序执行
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
//因为重排序,可能执行顺序为 2-1-3-4,1-3-2-4
//但绝不可能 2-1-4-3,因为这打破了依赖关系
//显然重排序对单线程运行是不会有任何问题
//而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了
八、Java内存模型
- 1.什么是java内存模型
- 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见
- 2.“内存模型”和“内存结构”的区别
- 内存模型:jmm,多线程相关的
- 内存结构:jvm,内存结构,如堆、栈、方法区等
- 3.主内存与本地内存
- JMM定义了线程和主内存之间的抽象关系
- 主内存:共享变量,线程之间的共享变量存储在主内存(main memory)中
- 本地内存:共享变量的副本,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本
- JMM定义了线程和主内存之间的抽象关系
- 如下图
- 多线程对count实现++的时候,并不是直接对共享变量count进行++
- 而是先对自己的本地内存count进行++,然后再刷新到主内存中
- a)起始的时候主内存count=0,这时候t1和t2线程同时执行
- b)此时t1的本地内存count=1,t2的本地内存count=1
- c)这时候t1和t2同时刷新到主内存中
- d)此时的主内存count=1
九、Volatile关键字
- 1.什么是Volatile
- 可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值
- 在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主内存
- 2.实际开发项目何时使用Volatile
- 只要是全局共享变量,全部都加上Volatile关键字
- 3.验证案例
- flag不加Volatile关键字,线程会一直运行,不会暂停
- 因为flag是在主线程中修改为false的,这时候并不会同步到主内存中,子线程中本地内存的flag还是true
- flag加Volatile关键字,3秒之后线程就停止了
- flag不加Volatile关键字,线程会一直运行,不会暂停
class ThreadDemo004 extends Thread {
public volatile boolean flag = true;
@Override
public void run() {
System.out.println("线程开始...");
while (flag) {
}
System.out.println("线程結束...");
}
public void setRuning(boolean flag) {
this.flag = flag;
}
}
public class App {
public static void main(String[] args) throws InterruptedException {
ThreadDemo004 threadDemo004 = new ThreadDemo004();
threadDemo004.start();
Thread.sleep(3000);
threadDemo004.setRuning(false);
System.out.println("flag已經改為false");
Thread.sleep(1000);
System.out.println("flag:" + threadDemo004.flag);
}
}
- 4.Volatile与Synchronized区别
- volatile
- 可以保证可见性,不能保证原子性(线程安全问题)
- 禁止重排序
- synchronized
- 既可以保证可见性,还可以保证原子性
- 没有禁止重排序
- synchronized是阻塞式的,同一时刻保证只能有一个线程在访问
- volatile
要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性