Java 多线程 - Synchronized 和变量并发访问

系统管理员 2021-04-19 15:06 803阅读 0赞

在非线程安全得情况下,多个线程对同一个对象中得实例变量进行并发访问时,产生得后果就是脏读,也就是取到得数据其实是被更改过得。

非线程安全问题存在于”实例变量”中,如果是方法内部得私有变量,则不存在”非线程安全”的问题。

1 Synchronized

1.1 synchronized方法

使用synchronized修饰方法时应注意使用同一个锁对象,否则会导致synchronized失效。

  1. public class ThreadTest {
  2. public static void main(String[] args) {
  3. Add add = new Add();
  4. Add add1 = new Add();
  5. ThreadAA threadAA = new ThreadAA(add);
  6. threadAA.start();
  7. ThreadBB threadBB = new ThreadBB(add1);
  8. threadBB.start();
  9. }
  10. }
  11. class ThreadAA extends Thread{
  12. private Add a;
  13. public ThreadAA(Add add){
  14. this.a = add;
  15. }
  16. @Override
  17. public void run(){
  18. a.add("a");
  19. }
  20. }
  21. class ThreadBB extends Thread{
  22. private Add b;
  23. public ThreadBB(Add add){
  24. this.b = add;
  25. }
  26. @Override
  27. public void run(){
  28. b.add("b");
  29. }
  30. }
  31. class Add{
  32. private int num = 0;
  33. //同步方法
  34. synchronized public void add(String username){
  35. try{
  36. if (username.equals("a")){
  37. num = 100;
  38. System.out.println("add a end");
  39. Thread.sleep(2000);
  40. }else {
  41. num = 200;
  42. System.out.println("add b end");
  43. }
  44. System.out.println(username + " name " + num);
  45. }catch (Exception e){
  46. e.printStackTrace();
  47. }
  48. }
  49. }

打印结果

  1. add a end
  2. add b end
  3. b name 200
  4. a name 100

从结果看出打印的顺序不是同步的,而是交叉的,这是因为关键字synchronized取得的锁都是对象锁。所以上面的示例中,那个线程先执行带synchronized关键字的方法,那个线程就持有该方法所属对象的锁,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。

验证synchronized方法持有的锁为对象锁

  1. //将上面的ThreadTest类中的main方法进行修改
  2. public class ThreadTest {
  3. public static void main(String[] args) {
  4. Add add = new Add();
  5. // Add add1 = new Add();
  6. ThreadAA threadAA = new ThreadAA(add);
  7. threadAA.start();
  8. ThreadBB threadBB = new ThreadBB(add);
  9. threadBB.start();
  10. }
  11. }

运行结果

  1. add a end
  2. a name 100
  3. add b end
  4. b name 200

此时看多的运行结果就是顺序打印的。

1.2 synchronized同步代码块

上面讲了同步方法,但是用synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么其他线程必须等待较长的时间。在这样的情况下,我们可以使用synchronized同步代码块来解决,使用synchronized同步代码块来包裹必须要同步执行的代码部分。

  1. public class ThreadFunction {
  2. public static void main(String[] args) {
  3. ObjFunction objFunction = new ObjFunction();
  4. FunA funA = new FunA(objFunction);
  5. funA.setName("a");
  6. funA.start();
  7. FunB funB = new FunB(objFunction);
  8. funB.setName("b");
  9. funB.start();
  10. }
  11. }
  12. class FunB extends Thread{
  13. private ObjFunction objFunction;
  14. public FunB(ObjFunction objFunction){
  15. this.objFunction = objFunction;
  16. }
  17. @Override
  18. public void run(){
  19. objFunction.objMethod();
  20. }
  21. }
  22. class FunA extends Thread{
  23. private ObjFunction objFunction;
  24. public FunA(ObjFunction objFunction){
  25. this.objFunction = objFunction;
  26. }
  27. @Override
  28. public void run(){
  29. objFunction.objMethod();
  30. }
  31. }
  32. class ObjFunction{
  33. public void objMethod(){
  34. try{
  35. System.out.println(Thread.currentThread().getName() + " start");
  36. synchronized (this) {
  37. System.out.println("start time = " + System.currentTimeMillis());
  38. Thread.sleep(2000);
  39. System.out.println("end time = "+ System.currentTimeMillis());
  40. }
  41. System.out.println(Thread.currentThread().getName() + " end");
  42. }catch (Exception e){
  43. e.printStackTrace();
  44. }
  45. }
  46. }

运行结果

  1. a start
  2. b start
  3. start time = 1559033466082
  4. end time = 1559033468083
  5. a end
  6. start time = 1559033468083
  7. end time = 1559033470084
  8. b end

可以看出,同步代码块外的代码是异步执行的,而同步代码块中的则是同步执行的。并且synchronized(this)的锁对象也是当前对象。

除了以this来作为锁对象,java还支持任意对象作为锁来实现同步功能,但需要注意的是作为同步监视器的必须是同一对象,否则运行结果就是异步调用了。

1.3 synchronized静态同步方法

关键字synchronized还可以应用到static静态方法上,这样的话就是一当前的*.java文件对应的Class类作为锁对象。

静态同步方法持有的锁对象=synchronized(class)

  1. public class ThreadTest {
  2. public static void main(String[] args) {
  3. ThreadAA threadAA = new ThreadAA();
  4. threadAA.start();
  5. ThreadBB threadBB = new ThreadBB();
  6. threadBB.start();
  7. }
  8. }
  9. class ThreadAA extends Thread{
  10. @Override
  11. public void run(){
  12. Add.add("a");
  13. }
  14. }
  15. class ThreadBB extends Thread{
  16. @Override
  17. public void run(){
  18. Add.add("b");
  19. }
  20. }
  21. class Add{
  22. private static int num = 0;
  23. //同步方法
  24. synchronized static public void add(String username){
  25. try{
  26. if (username.equals("a")){
  27. num = 100;
  28. System.out.println("add a end");
  29. Thread.sleep(2000);
  30. }else {
  31. num = 200;
  32. System.out.println("add b end");
  33. }
  34. System.out.println(username + " name " + num);
  35. }catch (Exception e){
  36. e.printStackTrace();
  37. }
  38. }
  39. }

运行结果

  1. add a end
  2. a name 100
  3. add b end
  4. b name 200

1.4 synchronized类

使用关键字synchronized修饰一个类,那么这个类中所有的方法都是同步方法,在编译得时候会把所有方法自动加上synchronized。

  1. class ClassName {
  2. public void method() {
  3. synchronized(ClassName.class) {
  4. // todo
  5. }
  6. }
  7. }

1.5 synchronized锁重入

synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。也就是说synchronized方法/代码块的内部调用本类的其他synchronized方法/代码块时,永远可以得到所。

  1. public class ThreadAgain {
  2. public static void main(String[] args) {
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. new Service().service1();
  7. }
  8. }).start();
  9. }
  10. }
  11. class Service{
  12. synchronized public void service1(){
  13. System.out.println("service1");
  14. service2();
  15. }
  16. synchronized private void service2() {
  17. System.out.println("service2");
  18. service3();
  19. }
  20. synchronized private void service3() {
  21. System.out.println("service3");
  22. }
  23. }

运行结果

  1. service1
  2. service2
  3. service3

2 volatile

关键字volatile的作用主要是使变量在多个线程间可见。

是强制从公共堆中取得变量的值,而不是从线程的私有数据栈中取得变量的值。在多线程中,栈与程序计数器是私有的,堆与全局变量是公有的。

先看代码

  1. public class MyVolatile {
  2. public static void main(String[] args) {
  3. try {
  4. RunThread runThread = new RunThread();
  5. runThread.start();
  6. Thread.sleep(2000);
  7. runThread.setRun(false);
  8. System.out.println("为runThread复制false");
  9. }catch (Exception e){
  10. e.printStackTrace();
  11. }
  12. }
  13. }
  14. class RunThread extends Thread{
  15. private boolean isRun = true;
  16. public boolean isRun() {
  17. return isRun;
  18. }
  19. public void setRun(boolean run) {
  20. isRun = run;
  21. }
  22. @Override
  23. public void run(){
  24. System.out.println("进入了run方法");
  25. while (isRun == true){
  26. }
  27. System.out.println("退出run方法,线程停止");
  28. }
  29. }

从控制台可以看到,线程并没有结束。这个问题就是私有堆栈中的值和工有堆栈中的值不同步造成的,想解决这样的问题使用volatile关键字就可以。

修改RunThread类中的代码

  1. class RunThread extends Thread{
  2. volatile private boolean isRun = true;
  3. public boolean isRun() {
  4. return isRun;
  5. }
  6. public void setRun(boolean run) {
  7. isRun = run;
  8. }
  9. @Override
  10. public void run(){
  11. System.out.println("进入了run方法");
  12. while (isRun == true){
  13. }
  14. System.out.println("退出run方法,线程停止");
  15. }
  16. }

再次运行,线程正常结束了。

虽然volatile关键字可以使实例变量在多线程之间可见,但是volatile有一个致命的缺点就是不支持原子性。

验证volatile不支持原子性

  1. public class IsAtomic {
  2. public static void main(String[] args) {
  3. MyAtomicRun[] myAtomicRuns = new MyAtomicRun[100];
  4. for (int i = 0;i<100;i++){
  5. myAtomicRuns[i] = new MyAtomicRun();
  6. }
  7. for (int i = 0;i<100;i++){
  8. myAtomicRuns[i].start();
  9. }
  10. }
  11. }
  12. class MyAtomicRun extends Thread{
  13. volatile public static int count;
  14. private static void count(){
  15. for (int i = 0;i<100;i++){
  16. count++;
  17. }
  18. System.out.println("count: " + count);
  19. }
  20. @Override
  21. public void run(){
  22. count();
  23. }
  24. }

打印输出

  1. //篇幅较长,没有全部粘贴
  2. count: 5000
  3. count: 4900
  4. count: 4800
  5. count: 4700
  6. count: 4600
  7. count: 4500
  8. count: 4400
  9. count: 4400

从输出的结果看,并没有输出我们理想状态中的10000。

对代码进行改进

  1. class MyAtomicRun extends Thread{
  2. volatile public static int count;
  3. //需要使用同步静态方法,这样是以class为锁,才能达到同步效果
  4. synchronized private static void count(){
  5. for (int i = 0;i<100;i++){
  6. count++;
  7. }
  8. System.out.println("count: " + count);
  9. }
  10. @Override
  11. public void run(){
  12. count();
  13. }
  14. }

打印输出

  1. count: 9300
  2. count: 9400
  3. count: 9500
  4. count: 9600
  5. count: 9700
  6. count: 9800
  7. count: 9900
  8. count: 10000

这一次输出的才是正确的结果。

关键字volatile主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获取最新的值使用,也就是多线程读取共享变量时可以获取最新的值。

2.1 volatile与synchronized的比较

  1. volatile时线程同步的轻量级实现,所以性能肯定比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。
  2. 多线程访问时volatile不会发生阻塞,而synchronized会出现阻塞。
  3. volatile能保证数据的可见性,但不能保证原子性;而synchronized既可以保证原子性,也可以间接保证可见性,因为synchronized是将私有内存和公共内存中的数据同步。
  4. volatile解决的是变量在多个线程之间的可见性;而synchronized解决的是多个线程之间访问资源的同步性。

2.2 变量在内存中的工作

像上面volatile关键字修饰的变量进行++运算这样的操作其实并不是一个原子操作,也就是非线程安全的。

i++操作步骤:

  1. 从内存中取出i的值
  2. 计算i
  3. 将i写入内存

如果在第二步计算的时候另一个线程也修改了i的值,那么这个时候就会出现脏数据。

  • read和load阶段:从主内存复制变量到当前线程的工作内存;
  • use和assign阶段:执行代码,改变共享变量值;
  • store和write阶段:用工作内存数据刷新主内存对应的变量值。

在多线程环境中,use和assign是多次出现的,但这个操作并不是原子性的,也就是读取阶段后,如果主内存中的变量值被修改,工作线程的内存因为已经加载过了,所以不会产生对应的变化,就造成了私有内存和公有内存中变量值不同步,计算出来的结果和预期就不一样,出现非线程安全问题。

对于volatile关键字修饰的变量,jvm只保证从主内存加载到工作内存中的值是最新的。

发表评论

表情:
评论列表 (有 0 条评论,803人围观)

还没有评论,来说两句吧...

相关阅读