线程安全定义
线程安全是一个非常重要的话题。Java通过 Thread 提供多线程环境,从相同的Object共享对象变量创建的多个线程,当线程用于读取和更新共享数据时,这可能导致数据不一致。
线程安全
数据不一致的原因是因为更新任何字段值不是原子过程,它需要三个步骤;
- 首先读取当前值
- 第二个读取必要的操作以获取更新的值
- 第三个将更新的值分配给字段引用
package coreofjava.javathread.threadsafety;
public class ThreadSafety {
public static void main(String[] args) throws InterruptedException {
ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
// 等待线程结束
t1.join();
t2.join();
System.out.println("正在处理 count = " + pt.getCount());
}
}
class ProcessingThread implements Runnable {
private int count;
@Override
public void run() {
for (int i = 1; i < 5; i++) {
processSomething(i);
count++;
}
}
public int getCount() {
return this.count;
}
private void processSomething(int i) {
try {
Thread.sleep(i * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述例子中,count增加了四次,有两个线程,所以值应该是8,但实际中是6或7或8
count++导致了数据损坏
让线程安全
- 同步是 java 中最简单和最广泛使用的线程安全工具
- 使用java.util.concurrent.atomic包中的Atomic Wrapper类,如AtomicInteger
- 使用java.util.locks包中的锁
- 使用线程安全集合类
- 使用带有变量的volatile关键字使每个线程从内存中读取数据,而不是从线程缓存中读取
Java 同步
JVM保证同步代码一次只能由一个线程执行
synchronized 用于创建同步,内部使用Object、Class上的锁来确保只有一个线程正在执行同步代码
- 同步在锁定或解锁资源时起作用,任何线程进入同步代码前,必须获取对象的锁定,当代码执行结束时,解锁可以被其他线程锁定的资源。同时,其他线程处于等待状态以锁定同步资源
- 2种方式使用synchronized, 一种是一个完整的方法同步,二是创建 synchronized 块
- 方法同步时,会锁定 Object,若方法是静态的,会锁定Class,因此最好使用synchronized块来锁定需要同步的方法的位移部分。
- 在创建synchronized块时,需要提供将获取锁的资源,可以是任意类或类的任意字段
- synchronized(this) 将在进入同步块之前锁定对象
- 应该使用最低级别的锁定,若有多个 synchronized块,并且其中一个锁了Object,则其他同步块也将无法由其他线程执行,当锁定一个Object,它会获取Object的所有字段。
- Java同步提供了性能成本的数据完整性,因此因此只有在绝对必要时才使用
- Java同步仅在同一个 JVM中工作,如果需要在多个JVM中锁定某些资源,将无法工作,可能需要处理一些全局锁定机制
- Java同步可能会导致死锁
- Java synchronized 关键字不能用于构造函数和变量
- 最好创建一个用于同步块的虚拟私有对象,它的引用就不能被任何其他代码更改,若过正在同步Object setter方法,则可以通过某些其他代码更改其引用,并执行 synchronized 块
- 不应该使用包含常量池的对象。 例如,String 不应该被用在同步方法中,如果任何其他代码给String加了锁,尽管两个代码不相关,它将要求要求锁住来自String池同一个引用对象,这将导致互相锁住。
下面代码我们需要进行改进来使得线程安全
package coreofjava.javathread.threadsafety;
public class HackerCode {
public synchronized void doSomething() {
System.out.println("Doing");
}
public static void main(String[] args) {
while (true) {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
正在尝试锁定 myObject 示例,一旦获得锁定,就永远不会释放它,这导致doSomething() 方法在等待锁定时阻塞,这将导致系统死锁并导致拒绝服务 (Dos)
Not safe example
package coreofjava.javathread.threadsafety;
import java.util.Arrays;
public class NotSafe {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1", "2", "3", "4", "5", "6"};
HashMapProcessor hmp = new HashMapProcessor(arr);
Thread t1 = new Thread(hmp, "t1");
Thread t2 = new Thread(hmp, "t2");
Thread t3 = new Thread(hmp, "t3");
long start = System.currentTimeMillis();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("Time taken = " + (System.currentTimeMillis()-start));
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class HashMapProcessor implements Runnable {
private String[] strArr = null;
public HashMapProcessor(String[] m) {
this.strArr = m;
}
public String[] getMap() {
return strArr;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
private void processArr(String name) {
for (int i = 0; i < strArr.length; i++) {
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] + ":" + name;
}
private void processSomething(int index) {
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序输出
Time taken = 15004
[1:t1, 2:t1:t3:t2, 3:t1, 4:t3, 5:t1:t3, 6:t1:t2:t3]
为何不是 1:t1:t2:t3, 2:t1,t2,t3, ...., 而是以上结果?
这是因为String数组值因共享数据而没有同步导致的损坏,我们将更改代码将使得线程安全
改进代码如下
package coreofjava.javathread.threadsafety;
import java.util.Arrays;
public class Safe extends NotSafe {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1", "2", "3", "4", "5", "6"};
SafeHashMapProcessor hmp = new SafeHashMapProcessor(arr);
Thread t1 = new Thread(hmp, "t1");
Thread t2 = new Thread(hmp, "t2");
Thread t3 = new Thread(hmp, "t3");
long start = System.currentTimeMillis();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("Time taken = " + (System.currentTimeMillis()-start));
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class SafeHashMapProcessor implements Runnable{
private final Object lock = new Object();
private String[] strArr = null;
SafeHashMapProcessor(String[] m) {
this.strArr = m;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
public String[] getMap() {
return strArr;
}
private void processArr(String name) {
for (int i = 0; i < strArr.length; i++) {
processSomething(i);
addThreadName(i, name);
}
}
private void processSomething(int index) {
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void addThreadName(int i, String name) {
synchronized (lock) {
strArr[i] = strArr[i] + ":" + name;
}
}
}
通过改进,程序运行输出为
Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]