Java线程(2) 学习笔记

本文详细介绍了Java中的线程安全特性,包括原子性、可见性和有序性。重点讨论了volatile关键字的作用,指出其能保证可见性但不保证原子性,并解释了内存屏障的概念。接着,文章探讨了final关键字防止构造器重排序的规则,以及CAS操作的原理和ABA问题。此外,还分析了可重入锁、死锁、线程池、FutureTask以及单例模式的多种实现,最后提到了JUC工具类和并发控制的相关概念。
摘要由CSDN通过智能技术生成

线程安全的三大特性

**原子性:**一个或多个操作,要么全部执行并且执行过程不被任何因素打断,要么不执行。

**可见性:**一个线程对某个共享变量修改,另外的线程可以立即看到。

**有序性:**程序执行的顺序按照代码的先后顺序进行。

volatile关键字

Volatile是一种轻量级的同步机制

1.保证可见性

2.不保证原子性 (例如 i++)

3.禁止指令重排序

防重排序

实例化一个对象可以分三个步骤:

  • 分配内存空间
  • 初始化对象
  • 将内存空间的地址赋值给对应的引用

但操作系统可以对指令进行重排序:

  • 分配内存空间
  • 将内存空间的地址赋值给对应的引用
  • 初始化对象

若是此流程,多线程下可能将一个未初始化的对象引用暴露出来。

Volatile的原理和实现机制

volatile变量的内存可见性是基于内存屏障实现的,内存屏障又称内存栅栏,是cpu的指令。在程序运行时,为了提高执行率,编译器和处理器会对指令重排序。为了保证在不同编译环境下又相同结果,通过加入特定类型的内存屏障来禁止重排序。

加入volatile关键字和没加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀的指令。

  0x000000000295157f: and    $0x37f,%rax
  0x0000000002951586: mov    %rax,%rdi
  0x0000000002951589: or     %r15,%rdi
  0x000000000295158c: lock  cmpxchg %rdi,(%rdx)  //lock 前缀的指令
  0x0000000002951591: jne    0x0000000002951a15
  0x0000000002951597: jmpq   0x00000000029515f8
  0x000000000295159c: mov    0x8(%rdx),%edi
  0x000000000295159f: shl    $0x3,%rdi
  0x00000000029515a3: mov    0xa8(%rdi),%rdi
  0x00000000029515aa: or     %r15,%rdi

lock前缀指令就是一个内存屏障

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
  • 将当前处理器缓存行的数据写会到内存上。
  • 如果是写回内存的操作,它会导致其他CPU中对应的缓存行无效。

Volatile应用场景

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中
  • 只有在状态真正独立于程序内其他内容时才能使用volatile

final关键字

写final域的重排序规则禁止对final域的写重排序到构造器最外。

  • JMM禁止编译器把final域的写重排序到构造函数之外;
  • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

CAS

介绍

Compare-And-Swap(对比交换),是cpu的一条原子指令,作用是让cpu先进行比较两个值是否相等,然后原子地更新某个位置的值。CAS是靠硬件实现的,JVM通过封装硬件平台的汇编调用。

CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

CAS操作是原子性的,所以多线程并发操作CAS更新数据时,可以不使用锁(synchronized)。

CAS方式为乐观锁。

实例

public class TestCas{
    private AtomicInteger = new AtomicInteger(0);//java提供AtomicInteger原子类,在多线程情况下实现数据的一致性
    public int add(){return i.addAndGet(1);}
}

ABA问题

CAS在操作值时,会去先检查值有没有发生变化,若无发生变化则更新值。但如果一个值从A变成B,在从B变成A,那个使用CAS进行检查时会发现它并没发生变化,但事实上已经发生了变化。

ABA问题解决可通过添加版本号。在变量前加版本号,例如1A-2B-3A。从JDK1.5开始,Atomic包提供了AtomicStampedReference解决ABA问题。

注意:Integer包装类,Integer.valueOf() 取值范围在(-128~127)之间都存入到IntegerCache.cache里面,取值相同的两个Integer可以使用==比较,若超出该取值范围,会new一个新的对象存入堆中,所以比较只能使用equals()去进行值对比。

        Integer i1 = Integer.valueOf(120);
        Integer i2 = Integer.valueOf(120);
        System.out.println(i1==i2);//true

        Integer i3 = Integer.valueOf(130);
        Integer i4 = Integer.valueOf(130);
        Integer i5 = new Integer(130);
        System.out.println(i3==i4);//false
        System.out.println(i3.equals(i4));//true
        System.out.println(i3.equals(i5));//true

CAS自旋

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

//模拟CAS自旋锁

AtmoicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
	Thread thread = Thread.currentThread();
	//自旋锁
	while(!atomicReference.compareAndSet(null),thread){}
}

//解锁
public void myUnLock(){
	Thread thread = Thread.currentThread();
	atomicReference.compareAndSet(thread,null);
}

可重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

//synchronized 可重入锁
class Phone1{
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName()+"sms()");
        call();
    }
     public synchronized void call(){
        System.out.println(Thread.currentThread().getName()+"call()");
    }
}

class Phone2{
    private ReentrantLock lock = new ReentrantLock();
    public void sms(){
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+"sms()");
        	call();
        }catch(Exception e){
            e.printException();
        }finally{
            lock.unlock()
        }
        
    }
     public void call(){
                 lock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+"call()");
        	call();
        }catch(Exception e){
            e.printException();
        }finally{
            lock.unlock()
        }
        
    }
}


public class Test{
    public static void main(String[] args){
        Phone1 phone1 = new Phone1();
        Phone2 phone2 = new Phone2();
        new Thread(()->{phone1.sms()},"1A").start();
        new Thread(()->{phone1.call()},"1B").start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{phone2.sms()},"2A").start();
        new Thread(()->{phone2.call()},"2B").start();
    }
}
//打印
//1Asms()
//1Acall()
//1Bsms()
//1Bcall()

//2Asms()
//2Acall()
//2Bsms()
//2Bcall()

死锁

在这里插入图片描述

//模拟产生死锁
class MyThread implements Runnable{
    private String lockA;
    private String lockB;
    
    public MyThread(String lockA,String lockB){
        this.lockA=lockA;
        this.lockB=lockB;
    }
    
    @Override
    public void run(){
		synchronized(lockA){
            System.out.println(Thread.currentThread().getName()+"lock:"+lockA+
                               "get:"+lockB);
            synchronized(lockB){     				System.out.println(Thread.currentThread().getName()+"lock:"+lockB+
                               "get:"+lockA);
            }
        }
    }
}

public class DemoDeadLock(){
    public static void main(String[] args){
        String lockA="lockA";
        String lockB="lockB";
        
        new Thread(()->{
            new MyThread(lockA,lockB),"Thread1"
        }).start();
        
          new Thread(()->{
            new MyThread(lockB,lockA),"Thread2"
        }).start();
    }
}

//运行后会出现
//Thread1lock:lockAgetlockB
//Thread2lock:lockBgetlockA
//彼此拿着对方的锁,同时也在等待对方释放锁,造成了死锁。

排查死锁

1.通过查看日志

2.通过查看堆栈信息(通过jps 获取进程号,再通过jstack获取该进程的堆栈信息)

UnSafe类(JNI)

Unsafe提供API大致可分内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。

AQS

AbstractQueuedSynchronizer(抽象的队列式同步器)

核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH:一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

JUC集合类

ConcurrentHashMap、

CopyOnWriteArrayList、

BlockingQueue、

SynchronizedQueue等

JUC工具类

CountDownLatch

CountDownLatch是一个计数器,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就 -1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

CyclicBarrier

CyclicBarrier类似CountDownLatch,不过它做的是[加法],需要里面的Thread全部运行结束才释放。

Semaphore

Semaphore底层是基于AbstractQueuedSynchronizer来实现的。Semaphore称为计数信号量,它允许n个任务同时访问某个资源,可以将信号量看做是在向外分发使用资源的许可证,只有成功获取许可证,才能使用资源。

JUC线程池

FutureTask(异步处理)

FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。

ExecutorService

ExecutorService集成Execurot接口,提供一种将任务提交和每个任务将如何运行的机制,包括线程使用的细节、调度等分离开来的方法。通常使用Executor而不是显式地创建线程。

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ForkJoin(分治)

Fork/Join 技术是分治算法(Divide-and-Conquer)的并行实现,它是一项可以获得良好的并行性能的简单且高效的设计技术。目的是为了帮助我们更好地利用多处理器带来的好处,使用所有可用的运算能力来提升应用的性能。

ThreadLocal(处理共享变量)

ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。

简单的理解(一个ThreadLoacl在一个线程中是共享的,在不同线程之间又是隔离的。每个线程都只能看到自己的变量的值)

例子
package ThreadTest;

public class ThreadLocalTest {
    //用ThreadLocal创建线程独享对象。ThreadLocal<T>
    private static ThreadLocal<Integer> num = new ThreadLocal<>();
    public static void main(String[] args) {
        num.set(1);
        new Thread(()->{
                    num.set(1);
                    System.out.println(num.get()+" "+Thread.currentThread().getName());
        }).start();
        new Thread(()->{
                    num.set(2);
                    System.out.println(num.get()+" "+Thread.currentThread().getName());

        }).start();
        System.out.println(num.get()+" "+Thread.currentThread().getName());
    }
}
//输出结结果
2 Thread-1
1 main
1 Thread-0

ThreadLoacl实现原理:

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

从源码中可以看到,向ThreadLocal存入一个值其实是向当前线程中的ThreadLocalMap存入值。也就是说其实并不是把数据存入到ThreadLocal对象中,而是以ThreadLocal这个实例存入到当前线程的Map里面,从而达到线程隔离。

单例模式

饿汉式

public class Hungry{
    //饿汉式的弊端,浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];    
    
    private Hungry(){}//单例模式一定要对构造器进行私有化处理
    private final static Hungry HUNGRY = new Hungry(); 
    
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

懒汉式

为了解决饿汉式的弊端

public class LazyMan{
	
    private LazyMan(){}
    
    private volatile static LazyMan lazyMan;//保证原子性
    
    //双重检测锁模式 (DCL-double checked locked)
    public static LazyMan getInstance(){
        if(lazyMan==null){
            synchronized (LazyMan.class){
                lazyMan = new LazyMan();//不是原子性操作 所以要加volatile
                /**
                **   1.分配内存空间
                **   2.初始化对象
                **   3.将内存空间的地址赋值给对应的引用
                **   若是编译器或者处理器对指令重排序 变成132执行顺序 多线程情况下LazyMan还没完成构建
                **/
            }
        }
        return lazyMan;
    }
    
}

静态内部类懒汉式

public class Holder{
    
    //对构造器私有化
    private Holder(){}
    
    public static Holder getInstance(){
        return InnerClass.HOLDER; 
    }
    
    public static class InnerClass{
        private final static Holder HOLDER = new Holder();
    }
}

反射机制破解单例模式(包括以上所有)

public static void main(String[] args) throw Exception{
    LazyMan lazyman1 = LazyMan.getInstance();
    Constructor<LazyMan> dec = LazyMan.class.getDeclaredCOnstructor(null);
    //无视私有构造器
    dec.setAccessible(true);
    LazyMan lazyman2 = dec.newInstance();
    System.out.println(lazyman1==lazyman2)//false;
    //lazyman1不等于lazyman2 破坏了单例模式
}

懒汉式优化

public class LazyMan{
	
    private LazyMan(){
        //三重检测了
        synchronized (LazyMan.class){//this指向实例对象 xxx.class指向该类信息
            if(LazyMan!=null){
                throw new RuntimeException("xxxxx");
            }
        }
    }
    
    private volatile static LazyMan lazyMan;//保证原子性
    
    //双重检测锁模式 (DCL-double checked locked)
    public static LazyMan getInstance(){
        if(lazyMan==null){
            synchronized (LazyMan.class){
                lazyMan = new LazyMan();//不是原子性操作 所以要加volatile
                /**
                **   1.分配内存空间
                **   2.初始化对象
                **   3.将内存空间的地址赋值给对应的引用
                **   若是编译器或者处理器对指令重排序 变成132执行顺序 多线程情况下LazyMan还没完成构建
                **/
            }
        }
        return lazyMan;
    }
    
}

枚举方法

枚举方法是没法用反射机制去

public enum EnumSingle{
	INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值