“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是”脏读”,也就是取到的数据其实是被更改过的。而”线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现”脏读”的现象.
目录
示例:
public class SynchronizedTest {
private static int count = 0;
private static final int SIZE = 1000;
private static void addNum(){
int temp=0;
count++;
if (count==SIZE*3){
System.out.println("success");
}
}
public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(()->{
for (int i=0;i<SIZE;i++) {
addNum();
}
});
Thread thread2 = new Thread(()->{
for (int i=0;i<SIZE;i++) {
addNum();
}
});
new Thread(()->{
thread1.start();
thread2.start();
for (int i=0;i<SIZE;i++) {
addNum();
}
}).start();
}
}
上面的代码是线程不安全的,多次执行发现多数时候是不会按照预期输出success
的
1、方法内的变量是线程安全的
“非线程”安全存在于“实例变量”中,如果是方法内部的私有变量,则是“线程安全”的。
可将addNum内函数体换成
private static void addNum(){
int safeVal = 0;
for (int i=0;i<SIZE;i++){
safeVal++;
}
System.out.println(safeVal);
}
执行,控制台输出的信息是符合预期的。
2、实例变量是线程不安全的
上述实验已经证明,多个线程同时访问没有同步的方法,同时操作业务对象中的实例变量count
,产生“脏读”现象,造成“线程不安全”问题。解决方法是,在方法addNum
前添加synchronized
关键字即可。
synchronized private static void addNum(){...}
加了关键字synchronized
后,当一个线程先执行到这个addNum
方法时,这个线程会持有该方法所属对象的锁,其他线程只能等待前面一个线程执行完方法释放锁后继续排队执行。
3、对象锁
关键字synchronized
取得的锁都是对象锁,不是把一段代码或方法当做锁,并且只是一个实例对象的锁,多个对象的话就是多个锁。
代码改为:
public class SynchronizedTest {
public static void main(String[] args) throws InterruptedException{
Test test1 = new Test("test1");
Test test2 = new Test("test2");
//线程1
Thread thread1 = new Thread(()->{
for (int i=0;i<SIZE;i++) {
test1.addNum();
}
});
//线程2
Thread thread2 = new Thread(()->{
for (int i=0;i<SIZE;i++) {
test2.addNum();
}
});
//线程3
new Thread(()->{
thread1.start();
thread2.start();
for (int i=0;i<SIZE;i++) {
test2.addNum();
}
}).start();
}
}
class Test{
private static final int SIZE = 1000;
private int count = 0;
private String tag;
Test(String tag){
this.tag=tag;
}
synchronized void addNum(){
if (++count%SIZE==0){
System.out.print(tag+",count="+count+";");
}
}
}
执行代码,输出test1,count=1000;test2,count=1000;test2,count=2000;
为正确结果,说明产生了两个对象锁,并且线程2与线程3获得的是实例test2的对象锁。但是这个时候必须明确一个点,对象锁仅仅的作用范围仅仅只是这个对象内被synchronized
修饰的方法或者代码块,其他方法是不受影响的。仅在Test
类新增
void addNum2(){
if (++count%SIZE==0){
System.out.print(tag+",count="+count+";");
}
并且修改线程3
new Thread(()->{
//thread1.start();
thread2.start();
for (int i=0;i<SIZE;i++) {
test2.addNum2();
}
}).start();
多次执行,会看到类似于test2,count=1589;test2,count=2000;
这种意想不到的结果,证明了addNum2
方法是不被锁的。
4、锁特性
需要提到“可重入锁”的概念:当1个线程获得了某个对象的锁,此时这个对象还没有释放,当其再次想要获取这个对象的锁的时候是还可以获取的。如果不可锁重入的话,就会造成死锁。死锁简单描述就是对象锁没有释放,线程获取不到锁这样的情况一直持续。
可重入锁也支持在父子类继承的环境中,但是同步是不具有继承性的。前者表示父子类各有一个被synchronized
修饰的方法,后者表示子类重写了父类带有synchronized
的方法,并且没有synchronized
修饰,子类的这个方法不具有同步的功能。
如下代码:
子类:
class Test extends Test1{
synchronized static void test1(String tag){
for (int i=0;i<SIZE;i++){
count++;
}
System.out.println("test1:tag="+tag+",count="+count);
mainTest(tag); //可重入锁也支持在父子类继承的环境中
test2(tag);
}
synchronized static void test2(String tag){
for (int i=0;i<SIZE;i++){
count++;
}
System.out.println("test2:tag="+tag+",count="+count);
test3(tag);
}
synchronized static void test3(String tag){
for (int i=0;i<SIZE;i++){
count++;
}
System.out.println("test3:tag="+tag+",count="+count);
}
static void test4(String tag){
System.out.println("test4:tag="+tag+",count="+count);
}
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
test1("thread1");
});
Thread thread2 = new Thread(()->{
test3("thread2");
});
new Thread(()->{
thread1.start();
thread2.start();
test4("thread3");//证明可以异步调用到非同步方法
}).start();
}
}
父类:
public class Test1 {
protected static int count = 0;
protected static final int SIZE = 100;
synchronized static void mainTest(String tag){
for (int i=0;i<SIZE;i++){
count++;
}
System.out.println("mainTest:tag="+tag+",count="+count);
}
}
结果输出:
test1:tag=thread1,count=100
mainTest:tag=thread1,count=200 //调用到父类同步方法,可重入锁也支持在父子类继承的环境中
test4:tag=thread3,count=100 //可异步调用到非同步方法
test2:tag=thread1,count=300
test3:tag=thread1,count=400
test3:tag=thread2,count=500 //可重入锁
以上实验证实了相关结论,同步是不具有继承性的,用之前的实验代码,新增子类重写方法,并且不加synchronized
,验证“线程不安全”结论即可。
5、出现异常,锁自动释放
public class Test1 {
protected static final int SIZE = 100;
synchronized static void test(String tag){
for (int i=0;i<SIZE;i++) {
if ("thread1".equals(tag) && i==SIZE-1) {
throw new RuntimeException();
}
}
System.out.println(tag+":success!");
}
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
test("thread1");
});
new Thread(()->{
thread1.start();
test("thread2");
}).start();
}
}
结果为
thread2:success!
Exception in thread "Thread-0" java.lang.RuntimeException
at thread.Test1.test(Test1.java:13)
at thread.Test1.lambda$main$0(Test1.java:20)
at java.lang.Thread.run(Thread.java:748)
说明当线程获得对象锁,执行时抛出异常,会自动释放掉这个锁。