一、基本概念
线程安全:当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
非线程安全:非线程主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”
二、线程安全举例
单纯地看线程安全的定义肯定会觉得非常晦涩。接下来通过下面的程序进行理解。
public class MyThread extends Thread{
private int count = 5 ;
//synchronized加锁
public void run(){
count--;
System.out.println(this.currentThread().getName() + " count = "+ count);
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
Thread t3 = new Thread(myThread,"t3");
Thread t4 = new Thread(myThread,"t4");
Thread t5 = new Thread(myThread,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
实例变量count的初始值为5,进行i--操作,我们的预期结果是输出 count = 4 、count = 3、count = 2、count = 1、 count = 0。然而实际输出结果如下:
实际数据结果合我们预期结果不相符。对比到定义就是这个类没有表现出正确的行为。所以说自定义类MyThread是线程不安全的。
当我们在方法run()前加上synchronized关键字,输出结果如下:
此时,我们的自定义类MyThread就是线程安全的。在这个例子中,我们实例化了一个对象myThread。开启了5个线程t1、t2、t3、t4、t5。(一个对象对应一把锁)
当多个线程访问myThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的),一个线程想要执行synchronized修饰的方法里的代码,首先是尝试获得锁,如果拿到锁,执行synchronized代码块中的内容:拿不到锁,这个线程就会不断尝试获得这把锁,知道拿到位置,而且是多个线程同时去竞争这把锁。也就是说会有锁竞争的问题。
这个例子中,一旦t2执行完释放锁,其他4个线程就会去抢这把锁。加入开启了1000个线程,一个线程释放锁之后,剩下999个线程去抢一把锁。将会导致CPU飙升,甚至宕机。
三、局部变量、实例变量与线程安全
方法内部的私有变量,也就是局部变量永远是线程安全的。如果多个线程共同访问1个对象中的实例变量,则有可能出现“非线程安全”问题(上例就是)。run()方法关键字synchronized修饰之后就变成同步方法;在多个线程同时访问同一个对象的同步方法时一定是线程安全的。
四、多个对象多个锁
public class MultiThread {
private int num = 0;
/** static */
public synchronized void printNum(String tag){
try {
if(tag.equals("a")){
num = 100;
System.out.println("tag a, set num over!");
Thread.sleep(1000);
} else {
num = 200;
System.out.println("tag b, set num over!");
}
System.out.println("tag " + tag + ", num = " + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//注意观察run方法输出顺序
public static void main(String[] args) {
//俩个不同的对象
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
m1.printNum("a");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
m2.printNum("b");
}
});
t1.start();
t2.start();
}
}
代码执行结果输出如下;
被synchronized修饰的方法printNum()是同步方法。但输出结果给人的感觉却是异步的,我们最初预想的输出情况是 tab a,set num over!之后紧跟 tag a,num=100。这是为何呢?
前面已经提及过一个对象一把锁。N个对象N个锁。上面的示例是两个线程分别访问同一个类的两个不同对象的同步方法。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当做锁。所以示例代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁。
因为有2个对象,所以有2个锁,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized同步方法体的内容。也就是时候两个线程t1、t2分别得到的是m1对象的锁和m2对象的锁,彼此之间是没有冲突的。
有一种情况则是相同的锁,即在static方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)),我们接着对上边的代码进行修改,在 synchronized void printNum()前加上static修饰,在int num=0前也加上static关键字,“类锁”输出结果如下:
六、对象锁的同步和异步
同步:synchronized
同步的概念就是共享,我们要牢牢记住“共享”两个字,如果不是共享的资源,就没有必要进行同步。同步的目的就是为了线程安全,其实对于线程安全来说,需要满足两个特性:①原子性(同步)、②可见性。
异步:asynchronized
异步的概念就是独立的、相互之间不受到任何约束,就好像我们学习http的时候,在页面发起Ajax请求,我们还可以继续浏览或操作页面的内容,两者之间没有任何关系。
七、脏读:
上面所有的例子,类都是只有一个同步方法。如果类中有1个同步方法,1个异步方法。
1)A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized方法。
2)A线程先只有object对象的Lock锁,B线程如果这在这时调用object对象中的synchronized方法则需等待。
public class DirtyRead {
private String username = "dmsd";
private String password = "123";
public synchronized void setValue(String username, String password){
this.username = username;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最终结果:username = " + username + " , password = " + password);
}
/**
* synchronized
*/
public void getValue(){
System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
}
public static void main(String[] args) throws Exception{
final DirtyRead dr = new DirtyRead();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
dr.setValue("z3", "456");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
}
上面的例子最终的输出结果为:
虽然在赋值时进行了同步,但在取值时出现了意外,也就是“脏读”。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改了。解决上述脏读的办法就是在getValue()方法前加上synchronized关键字。