非线程安全会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的结果就是脏读,也就是取到的数据是被更改过的。线程安全就是获得的实例变量的值是经过同步处理的。
方法内的变量是线程安全的
方法内的变量是线程安全的。非线程安全的问题存在于实例变量中,如果是方法内部的私有变量,不存在非线程安全问题。例子如下:
class HasMethodPrivateNum {
public void addI(String username){
try {
int num=0;
if(username.equals("a")){
num=100;
System.out.println("a set over");
Thread.sleep(2000);
}else{
num=200;
System.out.println("b set over");
}
System.out.println(username+" num = "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread {
private HasMethodPrivateNum numRef;
public ThreadA(HasMethodPrivateNum numRef){
super();
this.numRef=numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
class ThreadB extends Thread {
private HasMethodPrivateNum numRef;
public ThreadB(HasMethodPrivateNum numRef){
super();
this.numRef=numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
public class Run {
public static void main(String[] args) {
HasMethodPrivateNum numRef=new HasMethodPrivateNum();
ThreadA threadA=new ThreadA(numRef);
threadA.start();
ThreadB threadB=new ThreadB(numRef);
threadB.start();
}
}
复制代码
输出结果:
a set over
b set over
b num = 200
a num = 100
复制代码
可见,方法中的变量不存在非线性安全问题,是线程安全的。
实例变量非线程安全
实例变量是非线程安全的。如果多个线程共同访问一个对象中的实例变量,有可能出现非线程安全问题。用线程访问的对象中如果有多个实例变量,运行的结果可能出现交叉,如果只有一个实例变量,有可能出现覆盖的情况。在这种情况下,需要为操作该实例变量的方法加上synchronized关键字。在多个线程访问同一个对象中的同步方法时一定是线程安全的。
修改上述的代码,将第一个类中的addI()方法中的变量作为成员变量放到类中:
class HasSelfPrivateNum {
private int num=0;
synchronized public void addI(String username){
try {
if(username.equals("a")){
num=100;
System.out.println("a set over");
Thread.sleep(2000);
}else{
num=200;
System.out.println("b set over");
}
System.out.println(username+" num = "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
测试结果如下:
a set over
b set over
b num = 200
a num = 200
复制代码
可以发现,得到的结果是存在线程安全问题的。当为addI()方法加上synchronized关键字之后,测试结果如下:
a set over
a num = 100
b set over
b num = 200
复制代码
可以发现,不存在线程安全问题了。
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当作锁。当多个线程访问的是同一个对象时,哪个线程先执行带关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程只能等待。但是如果多个线程访问多个对象,则JVM会创建多个锁。
当一个对象存在同步方法a和非同步方法b,线程A和线程B分别访问方法a和方法b时,线程A先持有该对象的Lock锁,但是线程B可以异步调用该对象的非同步方法b。但是如果两个方法都是同步的方法,当A访问方法a时,已经持有了该对象的Lock锁,B线程此时调用该对象的另外的同步方法时,也需要等待,也就是同步。示例代码如下:
class MyObject {
synchronized public void methodA(){
try {
System.out.println("begin methodA in thread: "+Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end methodA in time:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB(){
try {
System.out.println("begin methodB in thread: "+Thread.currentThread().getName()+" time:"+System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end methodB");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private MyObject object;
public ThreadA(MyObject object){
super();
this.object=object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
class ThreadB extends Thread{
private MyObject object;
public ThreadB(MyObject object){
super();
this.object=object;
}
@Override
public void run() {
super.run();
object.methodB();
}
}
public class Run {
public static void main(String[] args) {
MyObject object=new MyObject();
ThreadA a=new ThreadA(object);
a.setName("A");
ThreadB b=new ThreadB(object);
b.setName("B");
a.start();
b.start();
}
}
复制代码
测试结果:
begin methodA in thread: A
begin methodB in thread: B time:1544263806800
end methodB
end methodA in time:1544263811800
复制代码
可以看出,线程A先得到了object对象的锁,但是线程B仍然异步调用了非同步方法。将methodB()添加了synchronized关键字后,测试结果为:
begin methodA in thread: A
end methodA in time:1544264023516
begin methodB in thread: B time:1544264023516
end methodB
复制代码
可以看到,A线程先得到object的锁,B线程如果此时调用objcet中的同步方法需要等待。
脏读
使用synchronized关键字可以实现多个线程调用同一个方法时,进行同步。虽然赋值时进行了同步,但是在取值时可能会出现脏读的情况,也就是在读取实例变量时,该值已经被其他线程更改过了。因此,需要在读取数据的方法也采用同步方法才可以。
锁重入
synchronized锁重入:在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁。也就是自己可以再次获取自己的内部锁。当一个线程获得了某个对象的锁,锁没释放且想要获取这个对象的锁的时候还是可以获取的。如果不可锁重入,会造成死锁。示例代码:
class Service {
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized public void service2(){
System.out.println("service2");
service3();
}
synchronized public void service3(){
System.out.println("service3");
}
}
class MyThread extends Thread{
@Override
public void run() {
Service service=new Service();
service.service1();
}
}
public class Run {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
}
}
复制代码
测试结果:
service1
service2
service3
复制代码
可重入锁也支持在父子类继承的环境中。当存在父子类继承关系时,子类可以通过“可重入锁”调用父类的同步方法。示例代码如下:
class Main {
public int i=10;
synchronized public void operateIMainMethod(){
try {
i--;
System.out.println("main print i = "+i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Sub extends Main{
public synchronized void operateISubMethod() {
try {
while(i>0){
i--;
System.out.println("sub print i="+i);
Thread.sleep(100);
this.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Sub sub=new Sub();
sub.operateISubMethod();
}
}
public class Run {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
}
}
复制代码
测试结果为:
sub print i=9
main print i = 8
sub print i=7
main print i = 6
sub print i=5
main print i = 4
sub print i=3
main print i = 2
sub print i=1
main print i = 0
复制代码
当一个线程执行的代码出现异常,其所持有的锁会自动释放。
同步不具有继承性
如果父类的方法是同步方法,但是子类重写了该方法,但是没有添加synchronized关键字,则在调用子类的方法时,仍然不是同步方法,需要在子类的方法中添加synchronized关键字才可以。