Java 并发编程(四)线程同步关键字volatile和synchronized

线程定义

线程,是程序执行流的最小单位。是进程中的一个实体,是被系统独立调用和分派的基本单元,线程自己不拥有系统资源,只是拥有在运行中必不可少的资源,但是可以与同属一个进程的其他线程共享全部资源,一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以兵法执行。单个程序中同时运行多个线程完成不同工作,称之为多线程。

java中并发包:java.util.concurrent(JUC)

##1. volatile关键字
作用:

  • 线程可见性,但不保证操作原子性
  • 防止指令重排

volatile修饰的变量/对象,每次修改后会cpu会通知其他线程,使得其他线程使用该变量/对象时从主内存中获取。
volatile不保证原子性,示例:
例子1:不使用volatile关键字

package com.multithread;

import java.util.concurrent.TimeUnit;

public class VolatileTest {
    public static int count = 0;

    public static void inc() {
        try {
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        count++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    VolatileTest.inc();
                }
            }).start();
        }
        System.out.println("count = " + VolatileTest.count);
    }
}

运行结果:count = 963,与期望值1000不同。


例子2:使用volatile关键字

package com.multithread;

import java.util.concurrent.TimeUnit;

public class VolatileTest {
    public volatile static int count = 0;

    public static void inc() {
        try {
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        count++;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    VolatileTest.inc();
                }
            }).start();
        }
        System.out.println("count = " + VolatileTest.count);
    }
}

运行结果:count = 980,与期望值1000不同。


1.1 分析原因

JVM在运行时对内存进行分配,其中一个内存区域是JVM虚拟机栈,每个线程在运行时都会有一个线程栈(TLAB–线程本地分配缓冲),其中保存了线程运行时变量值信息。当线程访问某一个对象的值的时候,首先通过对象引用找到对应堆内存的变量的值,然后把堆内存变量的具体值load到线程内存中,建立一个变量的副本,之后线程就不在和对象在堆内存变量值有任何关系,而是直接修改变量副本的值,在线程退出之前会将修改后副本的值会写到对象的堆内存中的值,这样在堆中对象的变量的值就发生变化了。
下图描述了这种交互:

线程内存与主内存交互

说明:

  • read and load 从主存复制变量到当前工作内存
  • use and assign 执行代码,改变共享变量值
  • store and write 用工作内存数据刷新主存相关内容

其中 use and assign 可以多次出现,但是这一些操作并不是原子性,也就是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。


对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值,在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6,线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。

2. 关键字synchronized

2.1 Java语言的关键字,同步锁。

synchronized修饰的对象有以下几种:

  • ,作用范围是synchronized后面括号括起来的部分,作用的对象是类的所有对象;
  • 静态方法,作用的范围是整个静态方法,作用的对象是类的所有对象;
  • 方法,称为同步方法,作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 代码块,称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。

作用:

  • 多线程的可见性,保证操作原子性
  • 防止指令重排

使用的锁:

  • 无锁 – new 出来的对象,没有任何同步操作
  • 偏向锁 – 对象上有一把锁,该对象就表现为偏向这个线程来获取锁
  • 轻量锁 – 对象上有多个锁竞争,就会升级为轻量锁
  • 重量锁 – 对象上有很多个锁竞争,而且是在高并发情况下,就会升级为重量级锁,即向cpu申请锁

锁标识:

  • 锁的标识是表现在Java 对象head的Mark Word中,使用后2-3位来标识对象的当前锁种类

2.2 例子

2.2.1 修饰类

用法如下:

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo work
      }
   }
}

示例:

public class SynClass implements Runnable {

    private static int count;

    public SynClass(int count) {
        this.count = count;
    }

    @Override
    public void run() {
        synchronized (SynClass.class) {
            try {
                for (int i = 0; i < 5; i++) {
                    System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

调试代码:

    SynClass synClassA = new SynClass(0);
    SynClass synClassB = new SynClass(0);
    new Thread(synClassA).start();
    new Thread(synClassB).start();

调试结果:

ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9

说明:synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。

2.2.2 修饰静态方法

用法如下:

public static synchronized void method() {
	// todo work
}

示例:

public class SynStaticMethod implements Runnable{

    private static int count;

    public SynStaticMethod() {
        count = 0;
    }

    public static synchronized void method() {
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
                TimeUnit.SECONDS.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public synchronized void run() {
        method();
    }
}

调试代码:

    SynStaticMethod synStaticMethodA = new SynStaticMethod();
    SynStaticMethod synStaticMethodB = new SynStaticMethod();
    new Thread(synStaticMethodA).start();
    new Thread(synStaticMethodB).start();

调试结果:

ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9

说明:静态方法是属于类的而不属于对象的。所以synchronized修饰的静态方法锁定的是这个类的所有对象,与类锁是一样的。

2.2.3 修饰非静态方法

synchronized修饰一个方法很简单,就是在方法的前面加synchronized, synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个方法。
用法如下:

public synchronized void method() {
	// todo work
}

示例:

定义一个类用来同步方法

public class NotStaticMethodService {
    private static int count;

    public NotStaticMethodService() {
        this.count = 0;
    }

    public synchronized void synMethod() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定义线程类

public class SynNotStaticMethod implements Runnable {

    private NotStaticMethodService notStaticMethodService;

    public SynNotStaticMethod(NotStaticMethodService notStaticMethodService) {
        this.notStaticMethodService = notStaticMethodService;
    }

    @Override
    public void run() {
        notStaticMethodService.synMethod();
    }
}

调试代码:

    NotStaticMethodService notStaticMethodService = new NotStaticMethodService();
        SynNotStaticMethod synNotStaticMethodA = new SynNotStaticMethod(notStaticMethodService);
        SynNotStaticMethod synNotStaticMethodB = new SynNotStaticMethod(notStaticMethodService);
        new Thread(synNotStaticMethodA).start();
        new Thread(synNotStaticMethodB).start();

调试结果:

ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9
2.2.4 代码块
  一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。

示例:

public class SynBlockRunnable implements  Runnable {
    private static  int count;

    public SynBlockRunnable() {
        count = 0;
    }

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

调试代码:

     SynBlockRunnable synBlockRunnable = new SynBlockRunnable();
     new Thread(synBlockRunnable).start();
     new Thread(synBlockRunnable).start();

调试结果:

ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9

说明:

当两个并发线程(thread1和thread2)访问同一个对象(SynBlockRunnable)中的synchronized代码块时,同一时刻只能有一个线程得到执行,另一个线程则受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

将上面调试代码修改如下:

 SynBlockRunnable synBlockRunnableA = new SynBlockRunnable();
 SynBlockRunnable synBlockRunnableB = new SynBlockRunnable();
 new Thread(synBlockRunnableA).start();
 new Thread(synBlockRunnableB).start();

此时调试结果为:

ThreadName:Thread-0, count = 0
ThreadName:Thread-1, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-1, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-1, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-0, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-0, count = 8

说明:

这时创建了两个SynBlockRunnable的对象synBlockRunnableA和synBlockRunnableB,线程Thread1执行的是synBlockRunnableA对象中的synchronized代码(run),而线程Thread2执行的是synBlockRunnableB对象中的synchronized代码(run);但synchronized锁定的是对象,这时会有两把锁分别锁定synBlockRunnableA对象和synBlockRunnableB对象,这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。

2.3 字节码实现

可通过javap -c命令来生成类的字节码中包含 monitorenter和monitorexit指令。synchronized关键字是基于这两个指令来实现锁的获取和释放过程。

2.4 Synchronized 的实现过程

  1. Java代码: synchronized 关键字
  2. 字节码:monitorenter monitorexit
  3. 锁升级:执行过程中自动升级
  4. 操作系统,cpu汇编指令:lock comxchg
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值