前面讲过,线程共享变量是非线程安全的,synchronized关键字可使方法变为线程安全的方法
一、线程安全问题
private CountNum countNum;
public MyThread(CountNum countNum) {
this.countNum = countNum;
}
@Override
public void run() {
countNum.add("a");
}
public class MyThread2 extends Thread {
private CountNum countNum;
public MyThread2(CountNum countNum) {
this.countNum = countNum;
}
@Override
public void run() {
countNum.add("b");
}
}
public class TestMain {
public static void main(String[] args) {
CountNum countNum=new CountNum();
MyThread myThread=new MyThread(countNum);
MyThread2 myThread2=new MyThread2(countNum);
myThread.start();
myThread2.start();
}
}
输出
a set over
b set over
name:b num:200
name:a num:200
两个线程同时访问一个没有同步的方法,a修改完num的值睡眠时,b这时又修改了num的值,a打印时的num值其实已经被b给修改了,这就是共享变量被多线程同事访问时候的问题。
二、synchronized
解决上述问题很简单,只需要在方法前加上synchronized关键字即可,即使a睡眠了,b也不会进入方法执行,此时方法是同步顺序执行的,输出如下
a set over
name:a num:100
b set over
name:b num:200
三、多个对象多个锁
把TestMain代码改成如下
public class TestMain {
public static void main(String[] args) {
CountNum countNum=new CountNum();
CountNum countNum2=new CountNum();
MyThread myThread=new MyThread(countNum);
MyThread2 myThread2=new MyThread2(countNum2);
myThread.start();
myThread2.start();
}
}
a set over
b set over
name:b num:200
name:a num:100
每个线程用不同的对象,此时代码又变为异步执行的,因为此时synchronized的锁是不同的对象。synchronized关键字取得的锁都是对象锁。
注意:
A线程获得某对象的锁时,其他线程可以以异步的方式调用该对象非synchronized类型的方法,但是其他线程调用其他synchronized的方法时需要等待
四、脏读
public class PublicVar {
private String name="b";
private String passwd="bb";
public synchronized void setValue(String name,String passwd){
try {
this.name=name;
Thread.sleep(5000);
this.passwd=passwd;
System.out.println("setName "+Thread.currentThread().getName()+" name="+name+" passwd="+passwd);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String getValue() {
System.out.println("getName "+Thread.currentThread().getName()+" name="+name+" passwd="+passwd);
return name;
}
}
public class MyThread extends Thread {
private PublicVar publicVar;
public MyThread(PublicVar publicVar) {
this.publicVar = publicVar;
}
@Override
public void run() {
publicVar.setValue("a", "aa");
}
}
public static void main(String[] args) throws InterruptedException {
PublicVar publicVar=new PublicVar();
MyThread myThread=new MyThread(publicVar);
myThread.start();
Thread.sleep(2000);
publicVar.getValue();
}
getName main name=a passwd=bb
setName Thread-0 name=a passwd=aa
此处出现脏读的原因是getValue方法并不是同步的,主线程启动完thread线程后休眠了2秒,thread线程在设置完name的值后休眠了5秒,主线程醒来后继续执行getValue方法,在休眠的两秒时间里,name已经被thread线程设置新值,但passwd还没有,所以出现了这种情况。
解决此问题的方法当然是给getValue方法也加上同步synchronized关键字
总结:
当A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确的将,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但其他线程可以随意调用其他非synchronized的方法,而其他线程调用声明synchronized关键字的非X方法时,也必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。
五、synchronized锁重入
可重入锁:比如A线程获得了某对象的锁,此时这个对象锁还没有释放,当其再次想获取这个对象的锁时还说可以获取的,一个synchronized方法/块的内部调用本类或父类的其他synchronized方法/块时,是永远可以得到锁的。如果不可锁重入的话,就会造成死锁。
当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。
注意:
重写父类的synchronized方法不具有同步效果,如需同步仍要手动加上synchronized关键字
六、死锁
public class DealThread implements Runnable {
public String name;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setName(String name) {
this.name = name;
}
@Override
public void run() {
if ("a".equals(name)) {
synchronized (lock1) {
try {
System.out.println("name=" + name);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("按lock1->lock2代码顺序执行了");
}
}
}
if ("b".equals(name)) {
synchronized (lock2) {
try {
System.out.println("name=" + name);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("按lock2->lock1代码顺序执行了");
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
DealThread dealThread=new DealThread();
dealThread.setName("a");
Thread thread=new Thread(dealThread);
thread.start();
Thread.sleep(1000);
dealThread.setName("b");
Thread thread2=new Thread(dealThread);
thread2.start();
}
查看死锁信息,进入jdk的bin目录执行jps
命令,输出如下,发现TestMain线程的id为8196
3904 Jps
10628 Launcher
8196 TestMain
9892 RemoteMavenServer
11432
使用jstack命令查看结果jstack -l 8196
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000002f1c978 (object 0x00000000d59a1d30, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000002f188d8 (object 0x00000000d59a1d40, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.yjj.chapter02.test04_syn4.DealThread.run(DealThread.java:43)
- waiting to lock <0x00000000d59a1d30> (a java.lang.Object)
- locked <0x00000000d59a1d40> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.yjj.chapter02.test04_syn4.DealThread.run(DealThread.java:30)
- waiting to lock <0x00000000d59a1d40> (a java.lang.Object)
- locked <0x00000000d59a1d30> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
七、锁对象改变
public class MyService {
private String lock="123";
public void testMethod(){
synchronized (lock){
try {
System.out.println(Thread.currentThread().getName()+" begin "+System.currentTimeMillis());
lock="456";
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+" end "+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadA extends Thread{
private MyService myService;
public ThreadA(MyService myService) {
this.myService = myService;
}
@Override
public void run() {
myService.testMethod();
}
}
ThreadA同ThreadB
public class TestMain {
public static void main(String[] args) throws InterruptedException {
MyService service=new MyService();
ThreadA a =new ThreadA(service);
a.setName("A");
ThreadB b =new ThreadB(service);
b.setName("B");
a.start();
Thread.sleep(50);
b.start();
}
}
可以看到A线程和B线程是异步执行的,虽然加了synchronized关键字,但是由于A线程竞争的是锁是“123”,得到锁后把锁改为了“456”,而B线程竞争的是“456”,此时并没有线程占用“456”的锁,于是B线程也得到了锁,就出现了如下的情况。
A begin 1537950658147
B begin 1537950658196
A end 1537950660147
B end 1537950660196
要注意的是:只要对象不变,即使对象的属性被改变,运行结果仍是同步的,也就是说把一个User对象当成锁,如果user的name属性发生变化,但是仍然是同一个user的话,仍会同步执行
八、volatile关键字
关于volatile以后写一篇专门的文章吧,现在只需要知道 volatile只保证可见性、有序性,并不保证原子性,i++并不是一个原子操作
九、原子类用不好也不完全安全
public class MyService {
public static AtomicLong atomicLong=new AtomicLong();
public void addNum(){
System.out.println(Thread.currentThread().getName()+"加了100后值="+atomicLong.addAndGet(100));
atomicLong.addAndGet(1);
}
}
@Override
public void run() {
myService.addNum();
}
public static void main(String[] args) {
MyService myService=new MyService();
AddCountThread addCountThread=new AddCountThread(myService);
Thread t1=new Thread(addCountThread);
Thread t2=new Thread(addCountThread);
Thread t3=new Thread(addCountThread);
Thread t4=new Thread(addCountThread);
Thread t5=new Thread(addCountThread);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
Thread-1加了100后值=100
Thread-3加了100后值=300
Thread-2加了100后值=200
Thread-4加了100后值=403
Thread-5加了100后值=503
输出结果如上,发现并不是我们想要的结果,造成这个的原因是addAndGet()方法调用不是原子的,虽然这个方法是一个原子方法,但是两个addAndGet()方法之间的调用却不是原子的,解决这一的问题必须要用同步,在addNum方法上synchronized或者用同步代码块包括在两个addAndGet()方法外