在非线程安全得情况下,多个线程对同一个对象中得实例变量进行并发访问时,产生得后果就是脏读,也就是取到得数据其实是被更改过得。
非线程安全问题存在于"实例变量"中,如果是方法内部得私有变量,则不存在"非线程安全"的问题。
public class ThreadTest { public static void main(String[] args) { Add add = new Add(); Add add1 = new Add(); ThreadAA threadAA = new ThreadAA(add); threadAA.start(); ThreadBB threadBB = new ThreadBB(add1); threadBB.start(); } } class ThreadAA extends Thread{ private Add a; public ThreadAA(Add add){ this.a = add; } @Override public void run(){ a.add("a"); } } class ThreadBB extends Thread{ private Add b; public ThreadBB(Add add){ this.b = add; } @Override public void run(){ b.add("b"); } } class Add{ private int num = 0; //同步方法 synchronized public void add(String username){ try{ if (username.equals("a")){ num = 100; System.out.println("add a end"); Thread.sleep(2000); }else { num = 200; System.out.println("add b end"); } System.out.println(username + " name " + num); }catch (Exception e){ e.printStackTrace(); } } } 复制代码
打印结果
add a end add b end b name 200 a name 100 复制代码
从结果看出打印的顺序不是同步的,而是交叉的,这是因为 关键字synchronized取得的锁都是对象锁 。所以上面的示例中,那个线程先执行带synchronized关键字的方法,那个线程就持有该方法所属对象的锁,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
验证synchronized方法持有的锁为对象锁
//将上面的ThreadTest类中的main方法进行修改 public class ThreadTest { public static void main(String[] args) { Add add = new Add(); // Add add1 = new Add(); ThreadAA threadAA = new ThreadAA(add); threadAA.start(); ThreadBB threadBB = new ThreadBB(add); threadBB.start(); } } 复制代码
运行结果
add a end a name 100 add b end b name 200 复制代码
此时看多的运行结果就是顺序打印的。
上面讲了同步方法,但是用synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么其他线程必须等待较长的时间。在这样的情况下,我们可以使用synchronized同步代码块来解决,使用synchronized同步代码块来包裹必须要同步执行的代码部分。
public class ThreadFunction { public static void main(String[] args) { ObjFunction objFunction = new ObjFunction(); FunA funA = new FunA(objFunction); funA.setName("a"); funA.start(); FunB funB = new FunB(objFunction); funB.setName("b"); funB.start(); } } class FunB extends Thread{ private ObjFunction objFunction; public FunB(ObjFunction objFunction){ this.objFunction = objFunction; } @Override public void run(){ objFunction.objMethod(); } } class FunA extends Thread{ private ObjFunction objFunction; public FunA(ObjFunction objFunction){ this.objFunction = objFunction; } @Override public void run(){ objFunction.objMethod(); } } class ObjFunction{ public void objMethod(){ try{ System.out.println(Thread.currentThread().getName() + " start"); synchronized (this) { System.out.println("start time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("end time = "+ System.currentTimeMillis()); } System.out.println(Thread.currentThread().getName() + " end"); }catch (Exception e){ e.printStackTrace(); } } } 复制代码
运行结果
a start b start start time = 1559033466082 end time = 1559033468083 a end start time = 1559033468083 end time = 1559033470084 b end 复制代码
可以看出, 同步代码块外的代码是异步执行的,而同步代码块中的则是同步执行的。并且synchronized(this)的锁对象也是当前对象。
除了以 this
来作为锁对象,java还支持任意对象作为锁来实现同步功能,但需要注意的是作为同步监视器的必须是同一对象,否则运行结果就是异步调用了。
关键字synchronized还可以应用到static静态方法上,这样的话就是一当前的*.java文件对应的Class类作为锁对象。
public class ThreadTest { public static void main(String[] args) { ThreadAA threadAA = new ThreadAA(); threadAA.start(); ThreadBB threadBB = new ThreadBB(); threadBB.start(); } } class ThreadAA extends Thread{ @Override public void run(){ Add.add("a"); } } class ThreadBB extends Thread{ @Override public void run(){ Add.add("b"); } } class Add{ private static int num = 0; //同步方法 synchronized static public void add(String username){ try{ if (username.equals("a")){ num = 100; System.out.println("add a end"); Thread.sleep(2000); }else { num = 200; System.out.println("add b end"); } System.out.println(username + " name " + num); }catch (Exception e){ e.printStackTrace(); } } } 复制代码
运行结果
add a end a name 100 add b end b name 200 复制代码
使用关键字synchronized修饰一个类,那么这个类中所有的方法都是同步方法,在这就不代码展示了。
synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。也就是说synchronized方法/代码块的内部调用本类的其他synchronized方法/代码块时,永远可以得到所。
public class ThreadAgain { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { new Service().service1(); } }).start(); } } class Service{ synchronized public void service1(){ System.out.println("service1"); service2(); } synchronized private void service2() { System.out.println("service2"); service3(); } synchronized private void service3() { System.out.println("service3"); } } 复制代码
运行结果
service1 service2 service3 复制代码
关键字volatile的作用主要是使变量在多个线程间可见。
是强制从公共堆栈中取得变量的值,而不是从线程的私有数据栈中取得变量的值。 在多线程中,栈与程序计数器是私有的,堆与全局变量是工有的。
先看代码
public class MyVolatile { public static void main(String[] args) { try { RunThread runThread = new RunThread(); runThread.start(); Thread.sleep(2000); runThread.setRun(false); System.out.println("为runThread复制false"); }catch (Exception e){ e.printStackTrace(); } } } class RunThread extends Thread{ private boolean isRun = true; public boolean isRun() { return isRun; } public void setRun(boolean run) { isRun = run; } @Override public void run(){ System.out.println("进入了run方法"); while (isRun == true){ } System.out.println("退出run方法,线程停止"); } } 复制代码
从控制台可以看到,线程并没有结束。这个问题就是私有堆栈中的值和工有堆栈中的值不同步造成的,想解决这样的问题使用volatile关键字就可以。
修改RunThread类中的代码
class RunThread extends Thread{ volatile private boolean isRun = true; public boolean isRun() { return isRun; } public void setRun(boolean run) { isRun = run; } @Override public void run(){ System.out.println("进入了run方法"); while (isRun == true){ } System.out.println("退出run方法,线程停止"); } } 复制代码
再次运行,线程正常结束了。
虽然volatile关键字可以使实例变量在多线程之间可见,但是volatile有一个致命的缺点就是不支持原子性。
验证volatile不支持原子性
public class IsAtomic { public static void main(String[] args) { MyAtomicRun[] myAtomicRuns = new MyAtomicRun[100]; for (int i = 0;i<100;i++){ myAtomicRuns[i] = new MyAtomicRun(); } for (int i = 0;i<100;i++){ myAtomicRuns[i].start(); } } } class MyAtomicRun extends Thread{ volatile public static int count; private static void count(){ for (int i = 0;i<100;i++){ count++; } System.out.println("count: " + count); } @Override public void run(){ count(); } } 复制代码
打印输出
//篇幅较长,没有全部粘贴 count: 5000 count: 4900 count: 4800 count: 4700 count: 4600 count: 4500 count: 4400 count: 4400 复制代码
从输出的结果看,并没有输出我们理想状态中的10000。
对代码进行改进
class MyAtomicRun extends Thread{ volatile public static int count; //需要使用同步静态方法,这样是以class为锁,才能达到同步效果 synchronized private static void count(){ for (int i = 0;i<100;i++){ count++; } System.out.println("count: " + count); } @Override public void run(){ count(); } } 复制代码
打印输出
count: 9300 count: 9400 count: 9500 count: 9600 count: 9700 count: 9800 count: 9900 count: 10000 复制代码
这一次输出的才是正确的结果。
关键字volatile主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获取最新的值使用,也就是多线程读取共享变量时可以获取最新的值。
像上面volatile关键字修饰的变量进行++运算这样的操作其实并不是一个原子操作,也就是非线程安全的。
i++操作步骤:
如果在第二步计算的时候另一个线程也修改了i的值,那么这个时候就会出现脏数据。
在多线程环境中,use和assign是多次出现的,但这个操作并不是原子性的,也就是读取阶段后,如果主内存中的变量值被修改,工作线程的内存因为已经加载过了,所以不会产生对应的变化,就造成了私有内存和公有内存中变量值不同步,计算出来的结果和预期就不一样,出现非线程安全问题。