本平台的文章更新会有延迟,大家可以关注微信公众号-顾林海,如果大家想获取最新教程,请关注微信公众号,谢谢!
“非线程安全”是指在多个线程对同一个对象中的实例变量进行并发访问,导致读取到的数据与预期不符,也就是“脏读”,而“线程安全”就是指获得的实例变量的值是经过同步处理的,不会出现“脏读”现象。
如果是方法内的私有变量就不会存在“非线程安全”问题,也就说“非线程安全”的问题存在于“实例变量”中,我们看下面这段代码:
public class Task {
private String name = "bill";
public void setName(int index) {
try {
Thread.sleep(2000);
switch (index) {
case 1:
name = "jack";
break;
case 2:
name = "rose";
break;
default:
name = "default";
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("name="+name);
}
}
复制代码
Task这个类很简单,内部只有一个setName方法,传入一个整型参数,如果是1,name就被赋值为jack,如果是2,name就被赋值为rose,最后打印name。接下来就把这个Task实例交给两个线程去处理。
public class ThreadFirst extends Thread {
private Task mTask;
public ThreadFirst(Task task) {
this.mTask = task;
}
@Override
public void run() {
super.run();
mTask.setName(1);
}
}
复制代码
public class ThreadSecond extends Thread {
private Task mTask;
public ThreadSecond(Task task) {
this.mTask = task;
}
@Override
public void run() {
super.run();
mTask.setName(2);
}
}
复制代码
两个线程类都差不多,唯一不同的地方就是调用mTask的setName方法分别传入1和2,如果执行这两个线程,是不是输出两个不同的name值,我们看Client代码:
public class Client {
public static void main(String[] args) {
Task task=new Task();
Thread threadFirst=new ThreadFirst(task);
Thread threadSecond=new ThreadSecond(task);
threadFirst.start();
threadSecond.start();
}
}
复制代码
看看打印结果:
name=jack
name=jack
复制代码
发现输出的结果和我们预期不一样,这就是“非线程安全”问题,如何解决呢,可以按照上一节Android小知识-关于多线程的基础知识了解下中提到的给setName方法前加上关键字synchronized,代码如下:
public class Task {
private String name = "bill";
synchronized public void setName(int index) {
try {
Thread.sleep(2000);
switch (index) {
case 1:
name = "jack";
break;
case 2:
name = "rose";
break;
default:
name = "default";
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("name=" + name);
}
}
复制代码
打印结果:
name=jack
name=rose
复制代码
添加关键字synchronized后,这个setName方法就是一个同步方法,多个线程执行该方法时是排队执行的。
现在Task类中只有一个 同步方法,再添加一个非同步方法:
public class Task {
private String name = "bill";
synchronized public void setName(int index) {
try {
Thread.sleep(2000);
switch (index) {
case 1:
name = "jack";
break;
case 2:
name = "rose";
break;
default:
name = "default";
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name);
}
public void getName() {
System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name);
}
}
复制代码
新添加了一个getName方法,打印的时候连线程名一起打印,方便我们查看,第一个线程代码不变,两个线程类如下:
public class ThreadFirst extends Thread {
private Task mTask;
public ThreadFirst(Task task) {
this.mTask = task;
}
@Override
public void run() {
super.run();
mTask.setName(1);
}
}
复制代码
public class ThreadSecond extends Thread {
private Task mTask;
public ThreadSecond(Task task) {
this.mTask = task;
}
@Override
public void run() {
super.run();
mTask.getName();
}
}
复制代码
Client代码如下:
public class Client {
public static void main(String[] args) {
Task task=new Task();
Thread threadFirst=new ThreadFirst(task);
threadFirst.setName("ThreadFirst");
Thread threadSecond=new ThreadSecond(task);
threadSecond.setName("ThreadSecond");
threadFirst.start();
threadSecond.start();
}
}
复制代码
代码没什么变化,创建一个Task实例,交由两个线程处理。
打印结果:
[ThreadSecond]name=bill
[ThreadFirst]name=jack
复制代码
按照预期应该是先执行setName方法打印jack,再执行getName方法打印jack,现在是先执行了getName方法,再执行setName方法,也就是说,ThreadFirst线程先持有了object对象的Lock锁,ThreadSecond线程可以以异步的方式调用objec对象中的非synchronized类型的方法。
现在我们给getName方法前也加上关键字synchronized:
synchronized public void getName() {
System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name);
}
复制代码
执行程序,打印:
[ThreadFirst]name=jack
[ThreadSecond]name=jack
复制代码
这样的话ThreadFirst先持有object对象的Lock锁,ThreadSecond线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步。
通过多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronized关键字来进行同步,虽然在赋值时进行了同步,但在取值时有可能出现“脏读”,发生“脏读”的情况是在读取实例变量时,此值已经被其它线程更改过了,看下面代码:
public class Task {
private String name = "bill";
private String password="12345";
synchronized public void setName(String name,String password) {
try {
this.name=name;
Thread.sleep(5000);
this.password=password;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name+" password="+password);
}
public void getInfo(){
System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name+" password="+password);
}
}
复制代码
setName是一个同步方法,传入姓名和密码,在赋值密码前先休眠5秒,最后打印用户名和密码,而getInfo是非同步方法,用来打印用户名和密码。
public class ThreadFirst extends Thread {
private Task mTask;
public ThreadFirst(Task task) {
this.mTask = task;
}
@Override
public void run() {
super.run();
mTask.setName("jack","poiuytr");
}
}
复制代码
ThreadFirst线程类很简单,就是调用mTask实例的setName方法,设置用户名为jack,密码为poiuytr。
public class Client {
public static void main(String[] args) {
Task task=new Task();
Thread threadFirst=new ThreadFirst(task);
threadFirst.setName("ThreadFirst");
threadFirst.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.getInfo();
}
}
复制代码
创建Task实例和线程实例threadFirst,执行threadFirst线程,休眠2秒后获取相关信息。
打印:
[main]name=jack password=12345
[ThreadFirst]name=jack password=poiuytr
复制代码
通过打印结果就可以看出数据出现了脏读,出现脏读的原因是因为getInfo方法并不是同步的,所以可以在任意时候调用,解决办法就是加上同步synchronized关键字。
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁,在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
用关键字synchronized声明方法在某些情况下是有弊端的,当某个线程调用同步方法执行一个长时间的任务,那么其他线程就必须等待比较长的时间,在这样的情况下可以使用synchronized同步语句块来解决,synchronized方法是对当前对象进行加锁,而synchronized代码块是对某一个对象进行加锁。在使用同步synchronized代码块时需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中的所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。
多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行的,也就是同步,阻塞的。为此Java提供“任意对象”作为“对象监视器”来实现同步的功能。这个任意对象大多数是实例变量及方法的参数,使用格式为synchronized(非this对象)。