java多线程和锁,自用,长文

1)并行并发

1.并行指多个事件在同一个时刻发生(多cpu);并发指在某时刻只有一个事件在发生,某个时间段内由于 CPU 交替执行,可以发生多个事件。

2.并行没有对 CPU 资源的抢占;并发执行的线程需要对 CPU 资源进行抢占。

3.并行执行的线程之间不存在切换;并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。

Java 中多线程运行的程序可能是并发也可能是并行,取决于操作系统对线程的调度和计算机硬件资源

2)进程与线程

1.进程

进程是操作系统的资源调度和分配的基本单位;

程序执行时的一个实例,也就是一个应用程序最少有一个进程;

每个进程都有独立的内存地址空间;

在多线程OS中,进程不是可执行的实体,也就是进程至少创建一个线程去执行代码。

2.线程

进程中的一个实体;

进程的一个执行路劲;

cpu任务调度和执行的基本单位;

线程里的程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行

每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其它线程无权访问

区别:

  •  本质:进程是操作系统资源分配的基本单位;线程是任务调度和执行的基本单位
  •  内存分配:系统在运行的时候会为每个进程分配不同的内存空间,建立数据表来维护代码段、堆栈段和数据段;除了 CPU 外,系统不会为线程分配内存,线程所使用的资源来自其所属进程的资源
  • 资源拥有:进程之间的资源是独立的,无法共享;同一进程的所有线程共享本进程的资源,如内存,CPU,IO 等
  •  开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行程序计数器和栈,线程之间切换的开销小
  • 通信:进程间 以IPC(管道,信号量,共享内存,消息队列,文件,套接字等)方式通信 ;同一个进程下,线程间可以共享全局变量、静态变量等数据进行通信,做到同步和互斥,以保证数据的一致性
  • 调度和切换:线程上下文切换比进程上下文切换快,代价小
  • 执行过程:每个进程都有一个程序执行的入口,顺序执行序列;线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制控制
  • 健壮性:每个进程之间的资源是独立的,当一个进程崩溃时,不会影响其他进程;同一进程的线程共享此线程的资源,当一个线程发生崩溃时,此进程也会发生崩溃,稳定性差,容易出现共享与资源竞争产生的各种问题,如死锁等
  • 可维护性:线程的可维护性,代码也较难调试,bug 难排查

进程与线程的选择:

  • 需要频繁创建销毁的优先使用线程。因为进程创建、销毁一个进程代价很大,需要不停的分配资源;线程频繁的调用只改变 CPU 的执行
  • 线程的切换速度快,需要大量计算,切换频繁时,用线程
  • 耗时的操作使用线程可提高应用程序的响应
  • 线程对 CPU 的使用效率更优,多机器分布的用进程,多核分布用线程
  • 需要跨机器移植,优先考虑用进程
  • 需要更稳定、安全时,优先考虑用进程
  • 需要速度时,优先考虑用线程
  • 并行性要求很高时,优先考虑用线程

Java 编程语言中线程是通过 java.lang.Thread 类实现的。

Thread 类中包含 tid(线程id)、name(线程名称)、group(线程组)、daemon(是否守护线程)、priority(优先级) 等重要属性。

3)用户线程,守护线程

1.守护线程是程序运行的时候在后台提供一种通用服务的线程。当所有用户线程都执行完毕是,进程会关闭所有守护线程,再关闭程序。

2.Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法

4)创建线程

1.继承Thead类

2.实现Runable接口

3.实现Callable接口,使用FutrueTask类创建线程

/**
 * 实现 Callable 接口,使用 FutureTask 类创建线程
 * @author ConstXiong
 */
public class TestCreateThreadByFutureTask {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//通过构造 FutureTask(Callable callable) 构造函数,创建 FutureTask,匿名实现接口 Callable 接口
		FutureTask<String> ft = new FutureTask<String>(new Callable<String>() {
			@Override
			public String call() throws Exception {
				return "ConstXiong";
			}
		});
		
		//Lambda 方式实现
//		FutureTask<String> ft = new FutureTask<String>(() ->  "ConstXiong");
		
		new Thread(ft).start();
		System.out.println("执行结果:" + ft.get());
	}
}

4.使用线程池进行创建

public class TestCreateThreadByThreadPool {

	public static void main(String[] args) {
		// 使用工具类 Executors 创建单线程线程池
		ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
		//提交执行任务
		singleThreadExecutor.submit(() -> {System.out.println("单线程线程池执行任务");});
		//关闭线程池
		singleThreadExecutor.shutdown();
	}
}

5)线程安全问题,原因,解决方案

问题

1.可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到

2.原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性

3.有序性:程序执行的顺序按照代码的先后顺序执行

原因

1.可见性:缓存导致的可见性问题,线程先读取内存中的变量到自己的工作内存,适当时期再写回去。

2.原子性:线程切换带来原子性问题,如i++,先读取再进行加运算,但是刚读取就被cpu切换了,另一个线程又修改了i。

3.有序性:编译优化会发生指令重排对此带来的有序性问题。

解决办法:

1.synchronized、volatile、LOCK,可以解决可见性问题

2.JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题

3.Happens-Before 规则可以解决有序性问题

6)线程的状态

新建(new出线程的时候)

可运行(包括正在运行和就绪)(调用start)RUNNABLE

阻塞BLOCKED:

    a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

等待WAITING:处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

定时等待:处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

死亡TERMINATED

几个方法的比较

  1. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
  2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
  3. t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
  4. obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
  5. obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

7)线程终止

1.Java 中 Thread 类有一个 stop() 方法,可以终止线程,不过这个方法会让线程直接终止,在执行的任务立即终止,未执行的任务无法反馈,所以 stop() 方法已经不建议使用。

2.当线程进入 runnable 状态之后,通过设置一个标识位,线程在合适的时机,检查该标识位,发现符合终止条件,自动退出 run () 方法,线程终止。

8)什么是线程池?有哪几种创建方式?

线程池,顾名思义,线程存放的地方。和数据库连接池一样,存在的目的就是为了较少系统开销,主要由以下几个特点:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗(主要)。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。

线程池的创建:

四种线程池的创建:

1.newCachedTheadPool创建一个可缓存线程池,如果线程池长度超过了处理需求,可灵活回收空闲线程。(《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 new ThreadPoolExecutor 实例的方式)

2.newFixedTheadPool创建一个定长线程池,可控制线程的最大并发,超出的线程进入队列。

3.newScheduledTheadPool创建一个定长线程池,支持定时和周期任务。

4.newSingleTheadPool创建一个单线程线程池,,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

9)ThreadPoolExecutor

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

corePoolSize:线程池核心线程数,即无论是否有需要执行的线程,都会有corePoolSize个线程存在。

maximumPoolSize最大线程数,线程池中最大工作线程数。

keepAliveTime:当线程数大于 corePoolsize时,线程处于空闲状态时,超过KeepAliveTime时间,则销毁该线程,保持运行的线程数=核心线程数。

unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。

workQueue:一个阻塞队列,提交的任务会放入该队列中。

theadFactory:线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3

handler:拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。

BlockingQueue:阻塞队列,有先进先出(注重公平性)和先进后出(注重时效性)两种,常见的有两种阻塞队列:

ArrayBlockingQueueLinkedBlockingQueue

ArrayBlockingQueue:基于定长数组实现,内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

LinkedBlockingQueue:基于链表的阻塞队列,内部 维护着一个数据缓冲队列,

LinkedBlockingQueueArrayBlockingQueue的主要区别

1.ArrayBlockingQueue初始化需要指定大小LinkedBlockingQueue确不需要,但是得避免,因为不指定可能会(生产者速度大于消费者)使得线程数无限增长,使得未消费系统内存就被消耗殆尽了。

2.ArrayBlockingQueue用一把锁控制并发,LinkedBlockingQueue俩把锁控制并发,锁的细粒度更细。即前者生产者消费者进出都是一把锁,后者生产者生产进入是一把锁,消费者消费是另一把锁。

3.ArrayBlockingQueue采用数组的方式存取,LinkedBlockingQueue用Node链表方式存取

10)Handler拒接策略

java提供了四种拒绝策略,也可以自定义(主要是要实现接口:RejectedExecutionHandler中的方法)

  • AbortPolicy:不处理,直接抛出异常。
  • CallerRunsPolicy:只用调用者所在线程来运行任务,即提交任务的线程。
  • DiscardOldestPolicy:LRU策略,丢弃队列里最近最久不使用的一个任务,并执行当前任务。
  • DiscardPolicy:不处理,丢弃掉,不抛出异常。

11)线程池五种状态

1.RUNNING:在这个状态的线程池能判断接受新提交的任务,并且也能处理阻塞队列中的任务

2.SHUTDOWN:处于关闭的状态,该线程池不能接受新提交的任务,但是可以处理阻塞队列中已经保存的任务,在线程处于RUNNING状态,调用shutdown()方法能切换为该状态。

3.STOP:线程池处于该状态时既不能接受新的任务也不能处理阻塞队列中的任务,并且能中断现在线程中的任务。当线程处于RUNNING和SHUTDOWN状态,调用shutdownNow()方法就可以使线程变为该状态

4.TIDYING:在SHUTDOWN状态下阻塞队列为空,且线程中的工作线程数量为0就会进入该状态,当在STOP状态下时,只要线程中的工作线程数量为0就会进入该状态。

5.TERMINATED:在TIDYING状态下调用terminated()方法就会进入该状态。可以认为该状态是最终的终止状态。

12)ThreadPoolExecutor的内部工作原理

execute方法:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
      //判断当前活跃线程数是否小于corePoolSize,如果小于,则调用addWorker创建线程执行任务
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
      //如果不小于corePoolSize,则将任务添加到workQueue队列。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
      //如果放入workQueue失败,则创建线程执行任务,如果这时创建线程失败(当前线程数不小于maximumPoolSize时),就会调用reject(内部调用handler)拒绝接受任务。
        else if (!addWorker(command, false))
            reject(command);
    }

即:

  1. 判断核心线程是否已满,是进入队列,否:创建线程
  2. 判断等待队列是否已满,是:查看线程池是否已满,否:进入等待队列
  3. 查看线程池是否已满,是:拒绝,否创建线程

我们可以看到三次尝试addWorker

  • 创建Worker对象,同时也会实例化一个Thread对象。在创建Worker时会调用threadFactory来创建一个线程。
  • 启动启动这个线程
private boolean addWorker(Runnable firstTask, boolean core) {
    // 第一步,cas操作保证正确的增加任务数
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        
        // 如果当前线程池不处于RUNNING状态,则不能添加任务
        if (rs >= SHUTDOWN && // 如果线程池状态rs >= SHUTDOWN,也就是非RUNNING状态,此时不接受新任务
            ! (rs == SHUTDOWN && //rs == SHUTDOWN ,此状态不接受新任务
               firstTask == null && 
               ! workQueue.isEmpty())) // 工作队列不为空
            return false;

        for (;;) {
            // 获取任务数量
            int wc = workerCountOf(c);
            //如果线程数 大于等于CAPACITY 添加任务失败
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))//尝试增加任务数量
                break retry;
            c = ctl.get();  // Re-read ctl
            // 如果当前的运行状态不等于rs,说明状态已被改变,返回第一个for循环继续执行
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    // 第二步 创建一个Worker,包装当前的任务,并启动该work中创建的线程,用于执行当前当前提交过来的任务
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);//新建一个worker,同时从ThreadFactory中创建一个新的线程
        final Thread t = w.thread;//
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);//放入worker集合
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {//worker添加成功,启动任务
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

这里分为两步,首先使用cas操作保证成功增加workerCount,然后将创建一个worker,将worker添加人worker池,启动worker,返回任务添加成功

13)线程的通信

共享内存和消息传递

1.同步(如synchronized,本质上就是“共享内存”式的通信

2.while轮询的方式(可增加volatile关键字保证可见性

3.wait/(notify/notifyAll)机制

4.管道通信(信息传递,使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信)

14)死锁出现的原因

1.因为系统资源不足。

2. 系统资源的竞争

系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。

3. 进程运行推进顺序不合适

进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。

15)死锁出现的必要条件

1.请求与等待条件

2.不可剥夺条件

3.循环等待条件

4.互斥条件

16)死锁的避免:银行家算法

17)死锁的预防

我们可以通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。

  1. 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
  2. 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
  3. 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

先不写了,写一道题睡觉去,明天再接着写

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

import java.util.ArrayList;
import java.util.Queue;
import java.util.LinkedList;
/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        if(pRoot == null)
            return new ArrayList<ArrayList<Integer> >();
        LinkedList<TreeNode> linkedList = new LinkedList<TreeNode>();
        ArrayList<ArrayList<Integer> > lists = new ArrayList<ArrayList<Integer> >();
        linkedList.add(pRoot);
        TreeNode tree = null;
        int num = 0;//每次肯定从尾拿但是要实现偶数右到左,奇数左到右
        while(linkedList.size() != 0){
            LinkedList<TreeNode> linkedList1 = new LinkedList<TreeNode>();
            ArrayList<Integer> list = new ArrayList<Integer>();
            while(linkedList.size() != 0){
                tree = linkedList.getLast();
                list.add(tree.val);
                if(num % 2 == 1){
                    if(tree.right != null){
                        linkedList1.add(tree.right);
                    }
                    if(tree.left != null){
                        linkedList1.add(tree.left);
                    }
                }
                else{
                    if(tree.left != null){
                        linkedList1.add(tree.left);
                    }
                    if(tree.right != null){
                        linkedList1.add(tree.right);
                    }
                }
                linkedList.removeLast();  
            }
            lists.add(list);
            linkedList = linkedList1;
            ++num;
        }
        return lists;
    }

}

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

/*
 public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ListNode deleteDuplication(ListNode pHead)
    {
        if(pHead == null || pHead.next == null)
            return pHead;
        ListNode l = pHead;
        ListNode r = pHead.next;
        ListNode root = null;
        ListNode rs = null;
        while(true){
            if(r != null && l.val == r.val){
                while(r!= null && l.val == r.val){
                    l = r;
                    r = r.next;
                }
                if(r != null){
                    l = r;
                    r = r.next;
                }
                else
                    break;
            }
            else{
                if(root == null){
                    root = rs = l;
                }
                else{
                    rs.next = l;
                    rs = l;
                }
                if(l.next == null)
                    break;
                l = r;
                r = r.next;
                rs.next = null;
            }
        }
        return root;
    }
}

18)synchronize

1.实现原理

synchronize可以保证方法或者代码块在运行的时候,同一时刻只有一个线程可以进入临界区,同时它可以保证共享变量的内存可见性。(非公平锁,重入锁

2.应用

普通同步方法,锁是当前实例对象 ,进入同步代码前要获得当前实例的锁

静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁

同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

3.注意

*一个线程正在访问一个带synchronize方法时,另一个线程要访问该方法或者其他synchronize方法时需要先获得锁。

*一个线程正在访问一个带synchronize方法时,当其他线程来访问非synchronized修饰的方法时是可以访问的。

*两个线程作用于不同的对象,获得的是不同的锁,所以互相并不影响。(这里不包括类锁)

*两个线程实例化两个不同的对象,但是访问的方法是静态的,两个线程发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。

synchronized作用于同步代码块时,主要看锁住的是类层次上的还是对象层次的。特别的类锁和对象锁也不冲突。

4.synchronized是一种内置锁/监视器锁

Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!

5.作用

synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)

synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

6.synchronize的优化-》锁升级

无锁:MarkWord标志位01,没有线程执行同步方法/代码块时的状态。偏向锁标记为0;

偏向锁:MarkWord标志位01(和无锁标志位一样),但是偏向锁标志为1;

轻量级锁:MarkWord标志位00。轻量级锁是采用自旋锁的方式来实现的,自旋锁分为固定次数自旋锁和自适应自旋锁。MarkWord标志位00。轻量级锁是采用自旋锁的方式来实现的,自旋锁分为固定次数自旋锁和自适应自旋锁。

线程竞争时,cas失败的线程会继续自旋,当然不可能让线程B一直自旋下去,自旋到一定次数(固定次数/自适应)就会升级为重量级锁。

重量级锁:通过对象内部监视器(monitor)实现,monitor本质前面也提到了是基于操作系统互斥(mutex)实现的,操作系统实现线程之间切换需要从用户态到内核态切换,成本非常高。

特别的:锁只可以升级不可以降级,但是偏向锁可以被重置为无锁状态。

19)Lock

Lock是一个接口,代码如下:

public interface Lock {
    void lock();//获得锁,如果锁已被其他线程获取,则进行等待。
    void lockInterruptibly() throws InterruptedException;//当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
    boolean tryLock();//尝试获得锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
    void unlock();//释放锁
    Condition newCondition();//线程线程协作相关
}




通常这么使用
Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}
可以配合各种信号量

20)从ReentrantLock讲Lock(https://segmentfault.com/a/1190000020541622?utm_source=tag-newest

ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

具体看链接0.0

1.互斥锁,可重入锁(获取锁的操作https://www.cnblogs.com/java-zhao/p/5134483.html

2.可实现公平锁和非公平机制(默认非公平)

3.响应中断

4.限时等待

5.锁绑定多个条件Condition,(一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。)

21)Condition接口常用方法

condition可以通俗的理解为条件队列。当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态,并且释放锁。

  2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。

  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。

  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。

  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。

  6. signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。

  7. signal()All :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

22)AQS(AbstractQueuedSynchronizer)(此内容总结均来自https://www.cnblogs.com/waterystone/p/4920797.html

AQS维护着一个volatile int state(代表共享资源)和一个CLH(FIFO线程等待队列)

state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

    提供公平机和非公平两种:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

    以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

    再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

  一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

独占:即acquire-release

Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus(volatile修饰的)则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

1:表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

0:新结点入队时的默认状态。

-1:表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

-2:表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

-3:共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

负值表示结点处于有效等待状态,而正值表示结点已被取消。

1.获得锁,以reentrantLock为例子

lock.lock()

public void lock() {

   sync.lock();

}

调用的是非公平的NonfairSync.lock()

类NonfairSync

final void lock() {

    if (compareAndSetState(0, 1))

        setExclusiveOwnerThread(Thread.currentThread());

    else

        acquire(1);

}

可以看到获取锁时,通过cas操作更新state状态,若成功,则获取到锁,否则,进行排队申请操作acquire

说到这里可以看到不但有公平非公平的实现还有独占和共享的实现,而公平非公平的基础是先确定使用的底层是独占的还是共享的先。

以独占为先:

我们可以看到如果cas不成功则进行 acquire(age)方法:


public final void acquire(int arg) {
 
    if (!tryAcquire(arg) &&
 
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 
        selfInterrupt();
 
}

 

 

先提前认识所涉及的函数:

tryAcquire(arg)再次尝试使用CAS获得锁,(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);

addWaiter:将当前线程插入到队列尾(即等待队列,Node数组形成的双向链表),并标记为独占模式;

acquireQueued:使线程阻塞在等待队列中获取资源(通过自旋,判断当前队列节点是否可以获取锁),一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

进入tryAcquire:

 

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

可以看到这个给子类去继承的,要实现自定义得到类需要实现它

进入addWaiter:

private Node addWaiter(Node mode) {
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);

    //尝试快速方式直接放到队尾。
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }

    //上一步失败则通过enq入队。
    enq(node);
    return node;
}

可以看到,我们需要先使用CAS尝试直接加入到队列尾部,成功则直接返回,

失败后:将进入enq进行自旋+CAS放入队列尾

enq:

private Node enq(final Node node) {
    //CAS"自旋",直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {//正常流程,放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {//即上个结点是否为tail尾节点
                t.next = node;
                return t;
            }
        }
    }
}

把当前线程放入队列尾后将执行acquireQueued(Node, int):在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功拿到资源
    try {
        boolean interrupted = false;//标记等待过程中是否被中断过

        //又是一个“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驱
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
            if (p == head && tryAcquire(arg)) {
                setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
                p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
                failed = false; // 成功获取资源
                return interrupted;//返回等待过程中是否被中断过
            }

            //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
        }
    } finally {
        if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

可以看到我们会在每次自旋的时候判断前驱节点是不是正在使用资源的线程,如果是则尝试获得锁,只有在前驱节点释放完资源的时候才会获取成功

获取成功后:将返回释放在等待过程中被中断过,中断过会执行acquire(age)中的selfInterrupt();(后面会讲到)

如果失败:则通过park()进入waiting状态,知道被unpark()唤醒

shouldParkAfterFailedAcquire(Node, Node):检查状态,看看自己是否真的可以去休息了(进入waiting状态)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)
        //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

总的逻辑是先判断前驱的状态即如果为-1则说明前驱结点也在等待资源,所以肯定轮不到当前线程,则放回进入waiting状态

否则再去清楚前驱中已经终中断得节点即waitstatus为1的结点,如果发现还是有正在正常等待的线程节点(即waitStatus状态值小于等于零)(CAS进行的,防止刚释放资源),除了一开始判断就需要进行等待,否则都放回fauls,即再次回到并继续进行acquireQueued的自旋进行获取锁;

再回到acquireQueued可以发现,在需要进入waitStatus时会先进行parkAndCheckInterrupt();

parkAndCheckInterrupt():此方法是让当前线程去休息等待的

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//调用park()使线程进入waiting状态
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。

Thread.interrupted()会清除当前线程的中断标记位。所以如果被中断过后返回true,此时再查看中断位是fauls,所以在返回的true的时候acquireQueued会再次标记为true。

总结acquireQueued()函数的具体流程:

结点进入队尾后,检查状态,找到安全休息点;

调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;

  1. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。

再回到最初的起点acquire(age);

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

总结:

1.调用自定义tryAcquire(arg)进行尝试获取资源,成功则直接返回

2.(1)失败了说明没有获取到资源,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;

3.acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

进入lock.unlock() (释放锁资源)

ReentrantLock中的
public void unlock() {
 
    sync.release(1);
 
}

可以看到释放资源也是继承AQS而来的,那我们再接着看release

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到头结点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒等待队列里的下一个线程
        return true;
    }
    return false;
}

release的代码就较为简单了,毕竟自由拥有锁资源的才会释放锁资源嘛!

1 protected boolean tryRelease(int arg) {
2     throw new UnsupportedOperationException();
3 }

跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg,可以思考一下共享模式就明白了),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

是放成功后直接找的头节点,再判断一下头节点线程的状态,如果可唤起则尝试唤起:

unparkSuccessor(h)此方法用于唤醒等待队列中下一个线程。下面是源码:

private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!

特别的如果获取锁的线程在release时异常了,没有unpark队列中的其他结点,队列中等待锁的线程将永远处于park状态,无法再被唤醒。

共享模式:

1 public final void acquireShared(int arg) {
2     if (tryAcquireShared(arg) < 0)
3         doAcquireShared(arg);
4 }

这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:

  1. tryAcquireShared()尝试获取资源,成功则直接返回;
  2. 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

doAcquireShared(int):

此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//加入队列尾部
    boolean failed = true;//是否成功标志
    try {
        boolean interrupted = false;//等待过程中是否被中断过的标志
        for (;;) {
            final Node p = node.predecessor();//前驱
            if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
                int r = tryAcquireShared(arg);//尝试获取资源
                if (r >= 0) {//成功
                    setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
                    p.next = null; // help GC
                    if (interrupted)//如果等待过程中被打断过,此时将中断补上。
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }

            //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

和独占模式差不多,但是把addwaiter和tryacqureQueue合并了,最大区别在于setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);//head指向自己
     //如果还有剩余量,继续唤醒下一个邻居线程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!

releaseShared()

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//尝试释放资源
        doReleaseShared();//唤醒后继结点
        return true;
    }
    return false;
}

此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。

doReleaseShared()

  此方法主要用于唤醒后继。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                unparkSuccessor(h);//唤醒后继
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)// head发生变化
            break;
    }
}

 简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全 部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用 sun.misc.Unsafe.park()本地方法

23)volatile底层实现

在JVM底层volatile是采用“内存屏障”来实现的。

24)Synchronize和lock的区别

1.Synchronize会释放锁,而lock必须手动释放(finally里面释放),而synchronize释放锁是由JVM自动执行的。即一个(lock)表现为API层面的互斥锁,一个表现为原生语法层面的互斥锁。

2.Lock有共享锁的概念,所以可以设置读写锁提高效率,synchronize不能。(两者都可重入)

3.Lock可以让线程在获取锁的过程中响应中断,而synchronize不会,线程会一直等待下去。lock.lockInterruptibly()方法会优先响应中断,而不是像lock一样优先去获取锁。

4.Lock锁的是代码块,synchronize还能锁方法和类。

5.Lock可以知道线程有没有拿到锁,而synchronize不能(会自旋或者阻塞)。

Synchronize是通过monitorenter和monitorexit来实现,monitor可实现监视器的功能,调用monitorenter就是尝试获取这个对象,获取成功则+1,离开则-1,如果是线程重入,则继续+1,即synchronize是可重入的。

先写这么多,过两天接着写

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值