1、线程安全
java的内存模型中有主内存和线程的工作内存之分,主内存上存放的是线程共享的变量(实例字段,静态字段和构成数组的元素),线程的工作内存是线程私有的空间,存放的是线程私有的变量(方法参数与局部变量)。线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性能并不是直接去修改主内存而是会在线程私有的工作内存中创建一份变量的拷贝(缓存),在工作内存上对变量的拷贝修改之后再把修改的值刷回到主内存的变量中去,JVM提供了8中原子操作来完成这一过程:lock, unlock, read, load, use, assign, store, write。
如果有多个线程同时在操作主内存中的变量,因为8种操作的非连续性和线程抢占cpu执行的机制就会带来冲突的问题,也就是多线程的安全问题。
线程安全:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。
线程不安全:多线程如果进行 `共享资源` 的抢夺, 就有可能会发生 `数据错误`.
解决方案 : 加锁
把 `共享资源` 操作的修改数据代码锁住, 不让多线程进入. 这样就可以解决数据错误的问题.
有如下方法可使用多线程安全
1. 同步代码块.
2. 同步方法.
3. `Lock接口 & ReentrantLock实现类` 互斥锁.
4.静态同步方法
下面通过二个案例来说明线程是不安全的。
需求 : 有一个航空公司, 售卖 100 张票. 有三个售票窗口同时卖同一家航空公司的 100 张票. 编写程序来模拟该行为.
方案一:继承Thread
package com.base.demo;
public class TicketsThread extends Thread {
private static int tickets = 100;
public TicketsThread(String name) {
super(name);
}
@Override
public void run() {
// 循环售票
while (true) {
// 判断
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 正在售出第 " + tickets + " 张票.");
tickets--;
} else {
// 没票了, 跳出循环, 结束线程
break;
}
}
}
}
测试类:
package com.base.demo;
public class TestTickets {
public static void main(String[] args) {
TicketsThread t1 = new TicketsThread("窗口一");
TicketsThread t2 = new TicketsThread("窗口二");
TicketsThread t3 = new TicketsThread("窗口三");
t1.start();
t2.start();
t3.start();
}
}
上面100张票多卖了二张
方案二:实现接口Runnable
package com.base.demo;
public class TicketsThread implements Runnable {
private static int tickets = 100;
@Override
public void run() {
// 循环售票
while (true) {
// 判断
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 正在售出第 " + tickets + " 张票.");
tickets--;
} else {
// 没票了, 跳出循环, 结束线程
break;
}
}
}
}
测式类:
package com.base.demo;
public class TestTickets {
public static void main(String[] args) {
TicketsThread task = new TicketsThread();
Thread t1 = new Thread(task,"窗口一");
Thread t2 = new Thread(task,"窗口二");
Thread t3 = new Thread(task,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
但也是一样、第100张票多卖了一张
2、同步代码块 synchronized
-
同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
-
锁对象 可以是任意类型。
-
多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
演示:
package com.base.demo;
public class TicketsThread implements Runnable {
// 属性
private int tickets = 100;
// 定义一把锁对象
Object lock = new Object();
// 2. 实现 run 抽象方法
@Override
public void run() {
// 循环
while (true) {
// 判断
// this 表示当前对象 : TicketsThread 类的对象,lock
// 接口方案中, this 表示任务类对象, 而任务类对象仅为一个, 所有保证数据的安全性.
synchronized (this) {
if (tickets > 0) {
// 让 CPU 切到其它线程继续执行...
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在售出第 " + tickets + " 张票.");
tickets--;
} else {
break;
}
}
}
}
}
测试类:
package com.base.demo;
public class TestTickets {
public static void main(String[] args) {
TicketsThread task = new TicketsThread();
Thread t1 = new Thread(task,"窗口一");
Thread t2 = new Thread(task,"窗口二");
Thread t3 = new Thread(task,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
正常了
3、同步方法 synchronized
-
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
演示:
package com.base.demo;
public class TicketsThread implements Runnable {
// 属性
private int tickets = 100;
// 2. 实现 run 抽象方法
@Override
public void run() {
// Object lock = new Object();
// 循环
while (true) {
// 判断
this.sellTickets();
if (tickets <= 0) {
break;
}
}
}
// 定义一个同步方法 :
// 请问 : 同步方法有锁对象吗 ??? 有. 谁是锁对象. this
public synchronized void sellTickets() {
if (tickets > 0) {
// 让 CPU 切到其它线程继续执行...
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在售出第 " + tickets + " 张票.");
tickets--;
}
}
}
测试类:
package com.base.demo;
public class TestTickets {
public static void main(String[] args) {
TicketsThread task = new TicketsThread();
Thread t1 = new Thread(task,"窗口一");
Thread t2 = new Thread(task,"窗口二");
Thread t3 = new Thread(task,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
4、Lock锁与ReentrantLock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
-
public void lock()
: 加同步锁。 -
public void unlock()
: 释放同步锁。
演示:
package com.base.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketsThread implements Runnable {
// 属性
private int tickets = 100;
// 定义一把 `互斥锁` 对象
Lock lock = new ReentrantLock();
// 2. 实现 run 抽象方法
@Override
public void run() {
// 循环
while (true) {
try {
// 加锁
lock.lock();
// 判断
if (tickets > 0) {
// 让 CPU 切到其它线程继续执行...
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在售出第 " + tickets + " 张票.");
tickets--;
} else {
break;
}
} finally {
// 释放锁
lock.unlock();
}
}
}
}
测试类:
package com.base.demo;
public class TestTickets {
public static void main(String[] args) {
TicketsThread task = new TicketsThread();
Thread t1 = new Thread(task,"窗口一");
Thread t2 = new Thread(task,"窗口二");
Thread t3 = new Thread(task,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
5、静态同步方法
说明 : 静态方法无需使用 `对象名` 调用. 而是直接使用 `类名` 调用.
请问 : 静态同步方法可以保证数据的安全性吗 ??? 安全. 锁对象为当前类的Class对象.
演示:
package com.base.demo;
public class TicketsThread implements Runnable {
// 属性
private static int tickets = 100;
// 2. 实现 run 抽象方法
@Override
public void run() {
// Object lock = new Object();
// 循环
while (true) {
// 判断
// this.sellTickets(); // 编译 : TicketsThread.sellTickets();
TicketsThread.sellTickets();
if (tickets <= 0) {
break;
}
}
}
// 定义一个静态同步方法 :
// 请问 : 静态同步方法有锁对象吗 ??? 有. 锁对象是谁吗 ?
// 锁对象为当前类 `Class对象`. (反射)
// 一个类在内存仅会被加载一次, 形成一个唯一的 Class 对象.
public static synchronized void sellTickets() {
if (tickets > 0) {
// 让 CPU 切到其它线程继续执行...
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 正在售出第 " + tickets + " 张票.");
tickets--;
}
}
}
测试类:
package com.base.demo;
public class TestTickets {
public static void main(String[] args) {
TicketsThread task = new TicketsThread();
Thread t1 = new Thread(task,"窗口一");
Thread t2 = new Thread(task,"窗口二");
Thread t3 = new Thread(task,"窗口三");
t1.start();
t2.start();
t3.start();
}
}