/**
课堂练习:
synchornized同步锁实现3个线程循环打印数字,使用线程1,打印1,2,3,4,5.
线程2,打印6,7,8,9,10.线程3,打印11,12,13,14,15.依次循环打印,直到
打印至60(提示:会使用到wait/notify/notifyAll方法)
**/
public class TestDemo10 {
public static int num = 1;//要打印的数字
public static int index = 0 ; //线程的计数器,用于判断当前线程是否为期望线程
public synchronized static void print(){
//需要判断当前线程是否是期望的线程
int name = Integer.parseInt(Thread.currentThread().getName());
while((index%3) != name){
//if只做一次判断,如果判断完发生线程的上下文切换则发生问题
//当前线程阻塞
try {
TestDemo10.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//线程1,2,3一次1~60
System.out.print("线程"+Thread.currentThread().getName()+ ":");
for(int i=0; i<5; i++)
{
if(i > 0){
System.out.print(",");
}
System.out.print(num++);
}
System.out.println();
//计数器+1
index=(index%3)+1; //1 2 3
//唤醒其他等待线程
TestDemo10.class.notifyAll(); //notify or notifyAll
}
public static void main(String[] args) {
for(int i=0; i<3; i++){
new Thread(""+i){
@Override
public void run() {
for(int j=0; j<4; j++){
print();
}
}
}.start();
}
}
}
volatile关键字
- 保证可见性、有序性
- 1、使用
- volatile修饰变量
- 2、volatile特征
- 1)保存内存可见性
- volatile修饰的变量不会缓存到工作内存中,每一次读取获取最新volatile变量
- 2)禁止指令重排序
- Java内存不会对volatile指令进行重排序,从而保证对volatile的执行顺序永远
- 是按照书写顺序执行的
- happens-before规则:volatile字段的写入操作happen before后续同一个字段
- 的读操作
- volatile修饰的变量产生的汇编代码,会存在一个lock前缀,相当于一个内存屏障
- 1) 它确保指令重排序的时候不会将其后面的指令拍到内存屏障之前的位置,也不会
- 前面的指令排到内存屏障之后。也就是执行到内存屏障这一指令时,在它前面的操作
- 已经全部执行完成
- 2) 它会强制性的将工作内存的修改立即写入主内存中
- 3) 如果是写操作,它会导致其他线程对应的工作内存中的值是无效值
- volatile使用场景
- 1)boolean标志位
- 2)单例模式双重检测锁
- 扩展知识:
- 1)happens-before原则 8条
happens-before原则 8条 - 2)重排序和禁止重排序介绍
- 计算机底层内存模型
- 在计算机中,所有运算操作都由cpu的寄存器完成,涉及到数据的读取的写入,主存和cpu
- cpu的处理速度和内存的访问速度差距太大啊
- cpu cache
- cpu -> cache -> main memory
- cpu缓存一致性问题
- 比如:i++
- 1)主内存中的数据复制到cache
- 2)cpu寄存器计算的时候从cache中读取和写入
- 3)cache刷新到主内存
- 单线程不会出现问题,多线程就会存在问题
- 为了缓存不一致性的问题,主要通过两种方式:
- 1)总线加锁
- 2)缓存一致性协议 (下去自己多加了解)
- 读取不做任何处理,写入发出信号通知其他cpu将变量的标志位置为无效状态
- 使得当前重排序过程发生改变,处理器会在计算之后对乱序执行的代码会进行重组,保证
- 结果的准确性
- volatile无法原子性
- 例:创建10个线程,分别执行1000次i++操作,期望程序最终的结果是10000
- 同时思考为什么出现这种结果?
- 小于10000的结果 volatile保证i的可见性,i++不具备原子性,volatile不保证i++原子性
- CountDownLatch的使用
- 有一个任务想要继续执行,但是必须要等待其他任务执行完
- volatile使用注意点
- volatile对应基本数据类型才有用
- volatile保证对象引用,保证对象内部的数据的可见性
*如 volatile只想array这个数组引用,当array指向从【10】变为【5】时只保证array
public volatile static String[] array; -> new String[10];
array -> new String[5];
**例:创建10个线程,分别执行1000次i++操作,期望程序最终的结果是10000
同时思考为什么出现这种结果?
*小于10000的结果 volatile保证i的可见性,i++不具备原子性,volatile不保证i++原子性
public class TestDemo9 {
public volatile static int i = 0;
public static void increase(){
i++;
}
public static void main(String[] args) {
for(int i=0; i<10; i++){
new Thread(){
@Override
public void run() {
for(int j=0; j<1000; j++){
increase();
}
}
}.start();
}
while(Thread.activeCount() > 2){
Thread.yield(); //自动让步一次cpu时间片
}
System.out.println(i);
}
}
这是用CountDownLatch达到和while循环实现让步一样的目的
public class TestDemo9 {
public volatile static int i = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
public static void increase(){
i++;
}
public static void main(String[] args) {
for(int i=0; i<10; i++){
new Thread(){
@Override
public void run() {
for(int j=0; j<1000; j++){
increase();
}
countDownLatch.countDown(); //countDownLatch中计数器-1
}
}.start();
}
try {
//调用await的线程阻塞,直到计数器为0才会继续执行
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
单例模式双重检测锁代码
最简单的使线程安全就是加synchronized(lock)
但存在问题,singleton不为null 频繁的加锁解锁会有效率问题
private volatile static SinhleTon = null; //实例
private SinhleTon(){
}
private static final Object lock = new Object();
public static SinhleTon getInstance(){
synchronized (lock){
//singleton不为null 频繁的加锁解锁会有效率问题
if(singleton == null){
singleton = new SinhleTon_doubleLock();
}
}
return singleton;
}
改进一下,将判断放到加锁的外面,判断是否为null以后再加锁,
但也存存在问题。如果A线程==null进入以后切换到B线程,B线程也为null并且创建了一个实例,然后再切回A线程。本就在if语句内的A线程会继续再创建一个实例。这就不符合单例模式了
private volatile static SinhleTon = null; //实例
private SinhleTon(){
}
private static final Object lock = new Object();
public static SinhleTon getInstance(){
if(singleton == null){
synchronized (lock){
singleton = new SinhleTon_doubleLock();
}
}
return singleton;
}
所以进一步改进,if语句内部再加一把锁,这里创建两个类测试一下。
构造函数有两个作用,先创建实例化对象,然后给成员变量初始化。
如果A线程正创建好实例化对象还未给成员变量初始化的时候切换到B线程,此时的B线程会以为全都做好了,于是调用a,b两个成员变量后出现空指针异常的错误。
解决办法就是在实例化对象前加上volatile关键字。
class A{
}
class B{
}
//懒汉式的单例
//double-check的方法可能引起的空指针异常
class MySingleton{
private volatile static MySingleton singleton = null; //实例
private A a;
private B b;
private MySingleton(){
//构造函数对当前实例a 和 b初始化
//new一个当前类的实例 成员变量初始化
//this.a = xx;
//this.b = xx;
}
private static final Object lock = new Object();
public static MySingleton getInstance(){
if(singleton == null){
synchronized (lock){
if(singleton == null){
//解决多线程环境,A/B线程都同时执行到synchronizedshang
singleton = new MySingleton();
}
}
}
return singleton;
}
}
public class TestDemo11 {
public static void main(String[] args) {
}
}