(二)对象以及变量的并发访问--synchronized的使用细节,用法
具体的记录synchronized关键的各种使用方式,注意事项。感觉一步一步跟我来都可以看懂滴
大致是按照以下思路进行书写的。黑体字可以理解为结论,
1.synchronized锁的是什么?
2.synchronized能够锁住所有方法吗?
3.synchronized能够用来锁住一个方法之中的部分代码吗?
4.synchronized能够锁住除了this以外的其他对象吗?有什么用?有什么需要注意的?
------------------------------------------------------------------------------------------正文------------------------------------------------------------------------------------------
1.synchronized锁的是什么?
首先,要明白非线程安全存在于实例变量之中,即大家都可以更改的变量,私有变量不存在线程安全问题。那么解决非线程安全问题,我们需要用用到 synchronized 来给某一个方法或者对象上锁,避免交叉访问的现象出现。那么synchronized到底锁的是什么呢?
先说结论,锁的是一个对象,一个类的实例,而不是将一个方法锁起来,如果想要在加上synchronized关键字之后同步运行,那多个线程访问的必须是同一个对象,这是锁的前提。也可以理解为加上synchronized关键字之后同步访问的前提是多个线程访问的是同一个资源,相当于他们是资源共享的。
用一个例子来说明:
twoNum.java是我们的测试类,里面有带锁的addNum方法,根据目前的线程名字来赋予num不同的值,a线程为100,b线程为200
MyThread.java:是自定义线程类,用于,run方法运行twoNum对象的addNum()方法
test.java:main函数
twoNum.java:
package 第二章;
public class twoNum {
private int num=0;
synchronized public void addNum(){
try{
if(Thread.currentThread().getName().equals("a")){
num=100;
System.out.println("a线程设置num的值");
Thread.sleep(2000);
}else{
num=200;
System.out.println("b线程设置num的值");
}
System.out.println(Thread.currentThread().getName()+" "+num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
MyThread.java,
package 第二章;
import 第二章.twoNum;
public class MyThread extends Thread {
private twoNum twonum;
public MyThread(twoNum temp){
super();
this.twonum=temp;
}
public void run(){
super.run();
twonum.addNum();
}
}
test.java:
package 第二章;
public class test {
public static void main(String[] args){
twoNum twonum = new twoNum();
MyThread threadA = new MyThread(twonum);
threadA.setName("a");
MyThread threadB = new MyThread(twonum);
threadB.setName("b");
threadA.start();
threadB.start();
}
}
运行上述代码,不出意料的,是线程安全的,并同步运行,因为我们给twonum对象的addNum方法上了锁,并且线程A,B是用一个twoNum初始化的,
更改test.java代码如下,用两个twoNum对象实例分别给A,B线程来初始化:
package 第二章;
public class test {
public static void main(String[] args){
twoNum twonum1 = new twoNum();
twoNum twonum2 = new twoNum();
MyThread threadA = new MyThread(twonum1);
threadA.setName("a");
MyThread threadB = new MyThread(twonum2);
threadB.setName("b");
threadA.start();
threadB.start();
}
}
运行结果如下:
可以看到现在A,B两个线程执行顺序虽然是异步的,但是数据仍然是正常的。为什么呢?很明显,因为有两个twoNum对象,所以有两个对象锁,A,B线程持有不同的锁,所以他们在访问时,访问的是不同对象,那当然能异步运行了,同时也有两个num变量,从属于不同的线程,A线程并不能够更改B线程当中的num变量,所以数据也是正常的。
上面的例子看得出,锁 关键字锁的是对象,
2.synchronized能够锁住所有方法吗?
那么synchronized锁的是整个对象里面的所有方法,还是怎么样呢?
先说结论:synchronized只能够锁住一个对象当中带锁的方法,并不是全部方法。可以理解为局部同步。
这就意味着假如A线程拿到了一个对象的锁,正在访问该对象之中的一个同步方法,这时候B线程也尝试拿到同一个对象锁,如果B线程访问的是该对象当中不带锁的方法,那么久能够拿到该锁并访问,如果访问的是该对象之中带锁的方法,那么B线程无法拿到该锁,只能等A线程释放锁之后才能拿到锁。
下面是一个例子:
修改twoNum.java如下:增加了一个没有锁的方法addNum2
package 第二章;
public class twoNum {
private int num=0;
synchronized public void addNum(){
try{
if(Thread.currentThread().getName().equals("a")){
num=100;
System.out.println("a线程设置num的值");
Thread.sleep(2000);
}else{
num=200;
System.out.println("b线程设置num的值");
}
System.out.println(Thread.currentThread().getName()+" "+num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void addNum2(){
try {
System.out.println(Thread.currentThread().getName() + "正在访问");
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("访问结束");
}
}
修改MyThread.java文件如下:两个线程,一个运行有锁的方法,另一个运行没有锁的
package 第二章;
import 第二章.twoNum;
class MyThread1 extends Thread {
private twoNum twonum;
public MyThread1(twoNum temp){
super();
this.twonum=temp;
}
public void run(){
super.run();
twonum.addNum();
}
}
class MyThread2 extends Thread {
private twoNum twonum;
public MyThread2(twoNum temp){
super();
this.twonum=temp;
}
public void run(){
super.run();
twonum.addNum2();
}
}
test.java:
public class test {
public static void main(String[] args){
twoNum twonum = new twoNum();
MyThread1 threadA = new MyThread1(twonum);
threadA.setName("a");
MyThread2 threadB = new MyThread2(twonum);
threadB.setName("b");
threadA.start();
threadB.start();
}
}
运行结果如图:
可以看到A对象拿到了锁,但是B线程仍然在同时访问了没有锁的addNum2()方法,证明了上述结论,其他线程可以在对象已经被占用的情况下可以异步访问同一个对象的没有锁的方法,但是有锁的方法却不行。有锁的不行这里不演示了,很简单。
理解了这一个概念,就可以解决有时候会碰到的脏读现象,
脏读很好理解,比如你现在执行一个setValue()函数,该函数更改两个值,username和password,当更改完username还没有更改password的时候,调用了getValue()方法获取这两个变量的值,那获取到的username是已经更改过的,但是password是没有更改的,这就出现了脏读。
这时候,运用我们所掌握的知识,给setValue()和getValue()方法都加上锁,这样子在setValue()执行结束之前,getValue()就无法拿到对象锁获取信息,这就解决了脏读问题。
接下来记录几个结论,比较容易理解就不展示例子了,只做记录:
1.出现异常,锁自动释放
2,同步方法不具有继承性,即父类有一个同步方法,那么他的子类如果有一个多态方法,那么子类中的方法想要同步必须加上synchronized关键字,不能继承;
3.synchronized能够锁住一个方法之中的部分代码吗?
可以看到,前面说的都是给整个方法上锁,但是想一下, 如果这个方法的执行时间会很久,A线程先拿到了锁,B线程如果要执行有锁的方法只能等待它执行完再执行,那么效率会很低,比如下面的例子:
twoNum.java:模拟一个任务
package 第二章;
public class twoNum {
private String data;
synchronized public void addNum(){
try{
System.out.println("开始");
Thread.sleep(3000);
data = Thread.currentThread().getName();
System.out.println("结束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
MyThread1.类,线程类:记录执行时间
class MyThread1 extends Thread {
long time1;
long time2;
private twoNum twonum;
public MyThread1(twoNum temp){
super();
this.twonum=temp;
}
public void run(){
super.run();
time1 = System.currentTimeMillis();
twonum.addNum();
time2=System.currentTimeMillis();
}
}
test.java:主函数
package 第二章;
public class test {
public static void main(String[] args){
twoNum twonum = new twoNum();
MyThread1 threadA = new MyThread1(twonum);
threadA.setName("a");
MyThread1 threadB = new MyThread1(twonum);
threadB.setName("b");
threadA.start();
threadB.start();
try{
Thread.sleep(6000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("A线程花费时间:"+(threadA.time2-threadA.time1));
System.out.println("B线程花费时间:"+(threadB.time2-threadB.time1));
}
}
运行结果如下:
可以看到B线程等待了3秒才执行,相当于多花费了3秒的时间。
那么怎么解决呢?使用synchronized同步代码块来解决
首先synchronized同步代码块就是说现在不给整个方法上锁,只给方法之中的部分关键代码上锁,这样当A线程拿到一个对象的锁时,B线程仍然可以访问相同对象之中没有上锁的代码块,但是不能访问上锁的代码块。简单来说,代码块锁synchronized锁的是一个对象的局部代码块,其他线程仍然可以在没有锁的情况下访问非同步代码块。
改变上述twoNum.java的代码,如下:
package 第二章;
public class twoNum {
private String data;
public void addNum(){
try{
System.out.println("开始");
Thread.sleep(3000);
String temp = Thread.currentThread().getName();
synchronized(this) {
System.out.println("线程"+temp+"赋值当中");
data=temp;
System.out.println("线程"+temp+"赋值结束");
}
System.out.println("结束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
只锁住关键的data赋值代码,其他代码块并不用锁,运行如下:
可以看到,开始结束时异步执行的,但是赋值却是同步的。证明了我们上面的结论,只同步执行synchronized锁住的代码块。
4.synchronized能够锁住除了this以外的其他对象吗?有什么用?有什么需要注意的?
你可能注意到了,上面synchronized(this) 里面锁住了this,这个意思就是说取到当前对象的锁,那么这个this能不能换成其他的对象呢?可以,他可以是任何对象,这个对象我们就叫做对象监听器。那么不同的对象监听器y有什么区别呢?
首先对象监听器总体分为两类:
1.this,即自身
2.非this对象,一般是实例变量或者方法的参数
那么第二种有什么用处呢?假设这种情况,现在有一个类,里面有很多个synchronized方法,执行起来确实是同步的,但是会受到阻塞。不过如果我们使用synchronized(非this对象)同步代码块来锁住一些代码块,这些代码块和其他被锁住的方法就是异步的了,因为他们锁的是不同对象,这样就提升了效率。
简单一句话,synchronized(非this对象)可以让锁住的代码块和其他方法异步执行,下面用程序进行演示:
twoNum.java:两个方法,一个上锁,另一个是synchronized(非this对象)同步代码块
package 第二章;
public class twoNum {
private String anything = new String();
public void addNum(){
try{
synchronized(anything) {
System.out.println("线程"+Thread.currentThread().getName()+"开始");
Thread.sleep(3000);
System.out.println("线程"+Thread.currentThread().getName()+"结束");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
synchronized public void addNum2(){
try{
System.out.println("线程"+Thread.currentThread().getName()+"开始");
Thread.sleep(3000);
System.out.println("线程"+Thread.currentThread().getName()+"结束");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
MyThread.java:定义了两个线程类,一个运行addNum()另一个运行addNum2()
package 第二章;
import 第二章.twoNum;
class MyThread1 extends Thread {
long time1;
long time2;
private twoNum twonum;
public MyThread1(twoNum temp){
super();
this.twonum=temp;
}
public void run(){
super.run();
twonum.addNum();
}
}
class MyThread2 extends Thread {
private twoNum twonum;
public MyThread2(twoNum temp){
super();
this.twonum=temp;
}
public void run(){
super.run();
twonum.addNum2();
}
}
test.java:
public class test {
public static void main(String[] args){
twoNum twonum = new twoNum();
MyThread1 threadA = new MyThread1(twonum);
threadA.setName("a");
MyThread2 threadB = new MyThread2(twonum);
threadB.setName("b");
threadA.start();
threadB.start();
}
}
运行结果如下:
可以看到他们是异步执行的,就是因为他们锁的是不同的对象。
不过要注意两点
1.java有字符串常量池,也就是如果有两个String对象,但是他们的值是相同的,那么当他们作为对象监听器时,他们是被看做同一个锁的。
2.synchronized如果加到一个静态方法上,那么它锁的就不是一个对象,而是整个类了。这时候可以理解为只锁了静态方法,该类的实例对象的锁还是可以正常拿到的。
下面看看java多线程死锁:
简单理解就是多个线程都在互相等待对方释放锁然后执行,双方互相持有对方的锁,这一般是程序bug,这块简单理解一下就行。