Java面试题复习整理(多线程)

2023-05-16

文章目录

    • 1.aio,nio,bio,epoll,select
    • 2.reactor模式
      • 介绍
      • Reactor软件工程
      • java代码
      • 总结
    • 3.Java中的cas乐观锁
    • 4.自旋锁是什么?
      • **自旋锁&&自适应自旋锁&&互斥锁**
    • 5.cas产生的aba问题及解决
      • 解决
    • 6.可重入锁是什么,有什么作用
    • 7.Java线程状态
    • 8.wait notify
      • 介绍
      • Demo-v1
      • Demo-v2
    • 9.关于Java中的synchronized
    • 锁实例方法v1
    • 锁实例方法v2
    • 锁静态方法
    • 锁代码块this
    • 锁代码块class
    • 锁实例&&静态
    • 详解
      • Demo
    • 10.Lock ReentrantLock ReentrantReadWriteLock?
      • ReentrantReadWriteLock Demo
    • 11.servlet controller 为什么非线程安全
    • 12.volatile
    • 13.condition && semaphore
      • Condition Demo
      • Semaphore Demo
    • 14.AQS原理源码
    • 15.threadlocal 用法场景及其内存泄漏
    • 16.threadpoolExecutor? 线程池
    • 17.concurrentHashMap源码(TODO)

1.aio,nio,bio,epoll,select

由于篇幅较长,整理到这里

https://blog.csdn.net/finalheart/article/details/107801496

2.reactor模式

印象中有netty和Redis用了这个模式。既然这么牛逼的中间件高并发框架

介绍

reactor模式是事件驱动的,有一个或多个并发输入源,有一个service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。
​​​​​​在这里插入图片描述

Reactor软件工程

下图是reactor的架构设计图
在这里插入图片描述

reactor是一种模式,或者说是一种软件工程的概念。 相当于设计模式,是一个概念不是一个实体。 不过JavaNIO的reactor实现真的有一种epoll的感觉,select,selectionKeys

  • Synchronous Event Demultiplexer 阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。相当于java中的selector selector.select selectionkeys
  • handle 即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。在Java中相当于一个 channel SocketCannel.open();
  • Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。 SelectionKey
  • Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。 interface
  • Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。 interfaceimpl

java代码

转自:https://blog.csdn.net/a953713428/article/details/64907250

服务端

package reactor;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @Author zhangyong
 * @Date 2020/8/6 14:59
 */
public class Server {

    //标识数字/
    private int flag = 0;
    //缓冲区大小/
    private int BLOCK = 4096;
    //接受数据缓冲区/
    private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
    //发送数据缓冲区/
    private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
    private Selector selector;


    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        int port = 7788;
        Server server = new Server(port);
        server.listen();
    }

    public Server(int port) throws IOException {
        // 打开服务器套接字通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 服务器配置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 检索与此通道关联的服务器套接字
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 进行服务的绑定
        serverSocket.bind(new InetSocketAddress(port));
        // 通过open()方法找到Selector
        selector = Selector.open();
        // 注册到selector,等待连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server Start----7788:");
    }

    // 监听
    private void listen() throws IOException {
        while (true) {
            // 选择一组键,并且相应的通道已经打开
            selector.select();
            // 返回此选择器的已选择键集。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                handleKey(selectionKey);
            }
        }
    }

    // 处理请求
    private void handleKey(SelectionKey selectionKey) throws IOException {
        // 接受请求
        ServerSocketChannel server = null;
        SocketChannel client = null;
        String receiveText;
        String sendText;
        int count = 0;
        // 测试此键的通道是否已准备好接受新的套接字连接。
        if (selectionKey.isAcceptable()) {
            // 返回为之创建此键的通道。
            server = (ServerSocketChannel) selectionKey.channel();
            // 接受到此通道套接字的连接。
            // 此方法返回的套接字通道(如果有)将处于阻塞模式。  接受请求
            client = server.accept();
            // 配置为非阻塞
            client.configureBlocking(false);
            // 注册到selector,等待连接
            client.register(selector, SelectionKey.OP_READ);  //将事件监听读操作,下次就绪时进行读取。
        } else if (selectionKey.isReadable()) {
            // 返回为之创建此键的通道。
            client = (SocketChannel) selectionKey.channel();
            //将缓冲区清空以备下次读取
            receivebuffer.clear();
            //读取服务器发送来的数据到缓冲区中
            count = client.read(receivebuffer);
            if (count > 0) {
                receiveText = new String(receivebuffer.array(), 0, count);
                System.out.println("服务器端接受客户端数据--:" + receiveText);
                client.register(selector, SelectionKey.OP_WRITE);       //读完数据,准备返回信息,将channel注册为写事件。供返回数据。
            }
        } else if (selectionKey.isWritable()) {
            //将缓冲区清空以备下次写入
            sendbuffer.clear();
            // 返回为之创建此键的通道。
            client = (SocketChannel) selectionKey.channel();
            sendText = "message from server--" + flag++;
            //向缓冲区中输入数据
            sendbuffer.put(sendText.getBytes());
            //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
            sendbuffer.flip();
            //输出到通道
            client.write(sendbuffer);
            System.out.println("服务器端向客户端发送数据--:" + sendText);
            client.register(selector, SelectionKey.OP_READ);
        }
    }
}


客户端

package reactor;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @Author zhangyong
 * @Date 2020/8/6 15:00
 */
public class Client {
    //标识数字/
    private static int flag = 0;
    //缓冲区大小/
    private static int BLOCK = 4096;
    //接受数据缓冲区/
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
    //发送数据缓冲区/
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
    //服务器端地址/
    private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
            "localhost", 7788);

    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        // 打开socket通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置为非阻塞方式
        socketChannel.configureBlocking(false);
        // 打开选择器
        Selector selector = Selector.open();
        // 注册连接服务端socket动作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        // 连接
        socketChannel.connect(SERVER_ADDRESS);
        // 分配缓冲区大小内存

        Set<SelectionKey> selectionKeys;
        Iterator<SelectionKey> iterator;
        SelectionKey selectionKey;
        SocketChannel client;
        String receiveText;
        String sendText;
        int count = 0;

        while (true) {
            //选择一组键,其相应的通道已为 I/O 操作准备就绪。
            //此方法执行处于阻塞模式的选择操作。
            selector.select();
            //返回此选择器的已选择键集。
            selectionKeys = selector.selectedKeys();
            //System.out.println(selectionKeys.size());
            iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                selectionKey = iterator.next();
                if (selectionKey.isConnectable()) {
                    System.out.println("client connect");
                    client = (SocketChannel) selectionKey.channel();
                    // 判断此通道上是否正在进行连接操作。
                    // 完成套接字通道的连接过程。
                    if (client.isConnectionPending()) {
                        client.finishConnect();
                        System.out.println("完成连接!");
                        sendbuffer.clear();
                        sendbuffer.put("Hello,Server".getBytes());
                        sendbuffer.flip();
                        client.write(sendbuffer);
                    }
                    client.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    client = (SocketChannel) selectionKey.channel();
                    //将缓冲区清空以备下次读取
                    receivebuffer.clear();
                    //读取服务器发送来的数据到缓冲区中
                    count = client.read(receivebuffer);
                    if (count > 0) {
                        receiveText = new String(receivebuffer.array(), 0, count);
                        System.out.println("客户端接受服务器端数据--:" + receiveText);
                        client.register(selector, SelectionKey.OP_WRITE);
                    }

                } else if (selectionKey.isWritable()) {
                    sendbuffer.clear();
                    client = (SocketChannel) selectionKey.channel();
                    sendText = "message from client--" + (flag++);
                    sendbuffer.put(sendText.getBytes());
                    //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
                    sendbuffer.flip();
                    client.write(sendbuffer);
                    System.out.println("客户端向服务器端发送数据--:" + sendText);
                    client.register(selector, SelectionKey.OP_READ);
                }
            }
            selectionKeys.clear();
        }
    }
}


总结

reactor是一种设计模式,并不是一个实体。基于事件来处理,一个线程负责监听,其他线程负责处理监听到事件时候的handle

3.Java中的cas乐观锁

compare and swap 比较和交换就是cas的核心

假设内存中的原数据V,旧的预期值A,需要修改的新值B。
比较 A 与 V 是否相等。(比较)
如果比较相等,将 B 写入 V。(交换)
返回操作是否成功。

只能有一个线程改成功,其他线程发现不一致就失败了,当然可以重试。乐观锁。

底层是使用unsafe包进行开发,unsafe是Java直接操作底层的一个后门。用AtomicInteger为例是如下调用链(jdk11)。

AtomicInteger integer = new AtomicInteger();
integer.getAndAdd(1);

public final int getAndAdd(int delta) {
    return U.getAndAddInt(this, VALUE, delta);
}
    
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {   //自旋
    int v;
    do {
        v = this.getIntVolatile(o, offset);
    } while(!this.weakCompareAndSetInt(o, offset, v, v + delta));

    return v;
}
    
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
    return this.compareAndSetInt(o, offset, expected, x);
}
    
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSetInt(Object var1, long var2, int var4, int var5);
//这个方法是比较内存中的一个值(整型)和我们的期望值(var4)是否一样,如果一样则将内存中的这个值更新为var5
//参数中的var1是值所在的对象,var2是值在对象(var1)中的内存偏移量,参数var1和参数var2是为了定位出值所在内存的地址。

接着就是汇编代码了,原理是将cas当做一个原子操作,并达到内存屏障的效果,读写相当于被volatile

参考:https://blog.csdn.net/v123411739/article/details/79561458

4.自旋锁是什么?

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

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务

自旋锁&&自适应自旋锁&&互斥锁

  • 自旋锁:是一种非阻塞锁,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。

  • 互斥锁:是阻塞锁,当某线程无法获取锁时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放锁后,操作系统会激活那个被挂起的线程,让其投入运行。

  • 自适应自旋锁:JDK1.6中引入了自适应的自旋锁。 自适应意味着自旋的时间不再是固定的, 而是由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚好成功获得锁, 并且在持有锁的线程在运行中, 那么虚拟机就会认为这次自旋也是很有可能获得锁, 进而它将允许自旋等待相对更长的时间。

public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<>();
    public void lock(){
        Thread t = Thread.currentThread();
        while (!cas.compareAndSet(null,t)){
        	//内存值V,旧的预期值E,要修改的新值U。当且仅当预期值E和内存值V相等时,将内存值V修改为U,否则什么都不做
        	//第一次进来预期值是null,在没unlock时被第一个线程置成t了,所以其他线程进来永远不是null就一直重试,直到是null为止
            //lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环
            //如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环
            //不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
        }
    }
    public void unlock(){
        Thread t= Thread.currentThread();
        cas.compareAndSet(t,null);
    }
}

5.cas产生的aba问题及解决

aba问题描述:
在多线程的环境中,线程a从共享的地址X中读取到了对象A。
在线程a准备对地址X进行更新之前,线程b将地址X中的值修改为了B。
接着线程b将地址X中的值又修改回了A。
最新线程a对地址X执行CAS,发现X中存储的还是对象A,对象匹配,CAS成功。

上述在部分场景可能会出问题

解决

使用AtomicStampedReference

Thread thread = new Thread(); //要cas的对象
private AtomicStampedReference<Thread> d = new AtomicStampedReference<Thread>(thread,1);

public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
    AtomicStampedReference.Pair<V> current = this.pair;
    return expectedReference == current.reference && expectedStamp == current.stamp && (newReference == current.reference && newStamp == current.stamp || this.casPair(current, AtomicStampedReference.Pair.of(newReference, newStamp)));
}

demo样例源代码:https://www.javazhiyin.com/54447.html

public class AtomicTest2 {
    private static AtomicInteger index = new AtomicInteger(10);
    static AtomicStampedReference<Integer> stampRef 
                            = new AtomicStampedReference(10, 1);
    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = stampRef.getStamp();
            System.out.println(Thread.currentThread().getName() 
                    + " 第1次版本号: " + stamp);
            stampRef.compareAndSet(10, 11,stampRef.getStamp(),stampRef.getStamp()+1);
            System.out.println(Thread.currentThread().getName() 
                    + " 第2次版本号: " + stampRef.getStamp());
            stampRef.compareAndSet(11, 10,stampRef.getStamp(),stampRef.getStamp()+1);
            System.out.println(Thread.currentThread().getName() 
                    + " 第3次版本号: " + stampRef.getStamp());
        },"张三").start();

        new Thread(() -> {
            try {
                int stamp = stampRef.getStamp();
                System.out.println(Thread.currentThread().getName() 
                        + " 第1次版本号: " + stamp);
                TimeUnit.SECONDS.sleep(2);
                boolean isSuccess =stampRef.compareAndSet(10, 12,
                        stampRef.getStamp(),stampRef.getStamp()+1);
                System.out.println(Thread.currentThread().getName() 
                        + " 修改是否成功: "+ isSuccess+" 当前版本 :" + stampRef.getStamp());
                System.out.println(Thread.currentThread().getName() 
                        + " 当前实际值: " + stampRef.getReference());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"李四").start();
    }
}

看上面的代码,使用了AtomicStampedReference,在cas操作的时候除了比较值还会比较一下版本号,只有当值和版本号都满足要求的时候才会执行赋值操作。

构造方法参数:

V initialRef: 泛型的初始值

int initialStamp 初始的版本号

compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)。

(1)第一个参数expectedReference:表示预期值。

(2)第二个参数newReference:表示要更新的值。

(3)第三个参数expectedStamp:表示预期的时间戳。

(4)第四个参数newStamp:表示要更新的时间戳。

6.可重入锁是什么,有什么作用

  • synchronized 和 ReentrantLock 都是可重入锁。ReentrantLock是公平锁(在锁上等待时间最长的线程将获得锁的使用权)并可以相应中断

  • 可重入锁的意义之一在于防止死锁。可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

  • 实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。如果同一个线程再次请求这个锁,计数器将递增;

  • 每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。

  • 关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁

  • 不可重入锁例如:自旋锁

7.Java线程状态

  • new出来—初始状态
  • start调用—就绪
  • run调用—运行
  • stop调用—dead
  • wait调用—阻塞 调用notify被随机唤醒 wait notify必须写在同步代码块中
  • sleep调用—阻塞 时间到 当前线程变成运行状态
  • join阻塞等待线程终止—阻塞 时间到 当前线程变成运行状态
  • yield调用—就绪 从运行变成就绪

8.wait notify

介绍

一个线程调用 Object(对象) 的 wait() 方法,使其线程被阻塞;另一线程调用 Object(对象) 的 notify()/notifyAll() 方法,wait() 阻塞的线程继续执行。

例如:A a = new A(); a.wait(); a.notify();这两个方法(同一个对象的)

wait/notify 方法

方法说明
wait()当前线程被阻塞,线程进入 WAITING 状态
wait(long)设置线程阻塞时长,线程会进入 TIMED_WAITING 状态。如果设置时间内(毫秒)没有通知,则超时返回
notify()通知同一个对象上已执行 wait() 方法且获得对象锁的等待线程
notifyAll()通知同一对象上所有等待的线程
  • 调用 wait 线程和 notify 线程必须拥有相同对象锁。
  • wait() 方法和 notify()/notifyAll() 方法必须在 Synchronized 方法或代码块中。
  • 由于 wait/notify 方法是定义在java.lang.Object中,所以在任何 Java 对象上都可以使用。

下面写一个demo就全明白了。

Demo-v1

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/13 14:36
 */
public class WaitNotify implements Runnable{

    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
//        WaitNotify wn = new WaitNotify();
//        new Thread(new WaitNotify(),"first").start();
//        new Thread(new WaitNotify(),"second").start();
        WaitNotify wn = new WaitNotify();
        //传入的对象是同一个,此时synchronized使用this锁的才是同一个对象,wait  notify都是作用于这个wn对象的、
        Thread t1 = new Thread(wn,"first");
        t1.start();
        new Thread(wn,"third").start();
//        t1.interrupt();   //wait  sleep这种让线程变成阻塞状态时,是可以被中断的。  解开注释可以看到现象是first直接被中断了。
        Thread.sleep(5000);
        new Thread(wn,"second").start();
    }

    @Override
    public void run() {
        if ("second".equals(Thread.currentThread().getName())){
            this.testNotify();
        }else {
            this.testWait();
        }
    }
    public void testWait(){
        synchronized (this){ //wait是释放锁阻塞,其他线程可以获取到这个锁。
            System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- waiting");
            try {
                this.wait(10000);       // 超过这个时间就继续执行,释放锁等待被唤醒
                Thread.sleep(2000);     // 验证当notifyAll触发后还是竞争锁的。具体哪个线程先跑取决于系统调度,不是公平锁。
                System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- end");
            } catch (InterruptedException e) {
                System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- Interrupted");
                e.printStackTrace();
            }
        }
    }
    public void testNotify(){
        synchronized (this){
            System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- notify");
            this.notifyAll();
            System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- end");
        }
    }
}

2020-08-14 22:45:03>>>>first— waiting
2020-08-14 22:45:03>>>>third— waiting
2020-08-14 22:45:08>>>>second— notify
2020-08-14 22:45:08>>>>second— end
2020-08-14 22:45:10>>>>first— end
2020-08-14 22:45:12>>>>third— end

Demo-v2

总结:this.wait(3000) 到点了,这个线程不会等待notify 而继续往下执行,此时还是会跟其他线程竞争锁

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/13 14:36
 */
public class WaitNotify implements Runnable{

    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
//        WaitNotify wn = new WaitNotify();
//        new Thread(new WaitNotify(),"first").start();
//        new Thread(new WaitNotify(),"second").start();
        WaitNotify wn = new WaitNotify();
        //传入的对象是同一个,此时synchronized使用this锁的才是同一个对象,wait  notify都是作用于这个wn对象的、
        Thread t1 = new Thread(wn,"first");
        t1.start();
        new Thread(wn,"third").start();
//        t1.interrupt();   //wait  sleep这种让线程变成阻塞状态时,是可以被中断的。  解开注释可以看到现象是first直接被中断了。
        Thread.sleep(10000);
        new Thread(wn,"second").start();
    }

    @Override
    public void run() {
        if ("second".equals(Thread.currentThread().getName())){
            this.testNotify();
        }else {
            this.testWait();
        }
    }
    public void testWait(){
        synchronized (this){ //wait是释放锁阻塞,其他线程可以获取到这个锁。
            System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- waiting");
            try {
                this.wait(3000);       // 超过这个时间就继续执行,释放锁等待被唤醒
                Thread.sleep(2000);     // 验证当notifyAll触发后还是竞争锁的。具体哪个线程先跑取决于系统调度,不是公平锁。
                System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- end");
            } catch (InterruptedException e) {
                System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- Interrupted");
                e.printStackTrace();
            }
        }
    }
    public void testNotify(){
        synchronized (this){
            System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- notify");
            this.notifyAll();
            System.out.println(dtf.format(LocalDateTime.now())+">>>>"+Thread.currentThread().getName()+ "--- end");
        }
    }
}

2020-08-14 22:43:09>>>>first— waiting
2020-08-14 22:43:09>>>>third— waiting
2020-08-14 22:43:14>>>>third— end
2020-08-14 22:43:16>>>>first— end
2020-08-14 22:43:19>>>>second— notify
2020-08-14 22:43:19>>>>second— end

总结:wait执行完就会继续竞争锁,无论是被notify唤醒还是时间到了。另外,wait的线程可以响应中断,因为wait时线程是 阻塞状态 有InterruptedException 异常(上述注释代码有标注)

9.关于Java中的synchronized

  • synchronized用的锁是存在Java对象头里的。
  • monitor object 充当着维护 mutex以及定义 wait/signal API 来管理线程的阻塞和唤醒的角色。
  • Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。
  • Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据和对齐填充,而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制
  • JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
  • 根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

synchronized分为对象锁和类锁

  • 对象锁:在java中每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。

  • 类锁:在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。

以上的实现分别作用于实例方法,静态方法,代码块中锁this或者class。 由于synchronized是可重入锁,就带上这个性质进行写demo了,话不多说看例子吧。

锁实例方法v1

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/8 17:21
 */
public class SyncTest implements Runnable{

    private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void run() {
        try {//调用synchronized实例方法,两次调用的都是对象t
            if ("one_thread".equals(Thread.currentThread().getName())){
                System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
                sync1();
            }else {
                System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
                sync2();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void sync1() throws InterruptedException {

        System.out.println("sync1"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
        Thread.sleep(1000);
        sync2();
        System.out.println("sync1"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public synchronized void sync2() throws InterruptedException {

        System.out.println("sync2"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
        Thread.sleep(1000);
        System.out.println("sync2"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public static void main(String[] args) {
        SyncTest t = new SyncTest(); //传入的对象是一个t
        Thread thread = new Thread(t,"one_thread");  //thread传入的runnable会被执行
        Thread thread1 = new Thread(t,"two_thread");
        thread.start();
        thread1.start();
    }
}

执行结果
one_threadrun on > 2020-08-09 17:12:30
two_threadrun on > 2020-08-09 17:12:30
sync1one_thread start>>>>2020-08-09 17:12:30
sync2one_thread start>>>>2020-08-09 17:12:31
sync2one_thread end >>>>2020-08-09 17:12:32
sync1one_thread end >>>>2020-08-09 17:12:32
sync2two_thread start>>>>2020-08-09 17:12:32
sync2two_thread end >>>>2020-08-09 17:12:33

总结:如上代码,两个线程(one,two)使用的是一个对象t ,在执行同步方法时会进行同步,先执行到达的线程(one)。然后占有锁,由于synchronized是可重入锁,在sync1方法中我又调用了sync2,目的是证明可重入(当前线程可以再调带锁实例方法,计数器栈深+1),通过执行结果可以判断出two是等到线程one执行完释放锁才能获取锁运行的。

锁实例方法v2

锁实例方法,如果调用的线程是不同对象呢?看下面demo(只改了main里面的代码,不同对象)

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/8 17:21
 */
public class SyncTest implements Runnable{

    private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void run() {
        try {//调用synchronized实例方法,两次调用的都是对象t
            if ("one_thread".equals(Thread.currentThread().getName())){
                System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
                sync1();
            }else {
                System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
                sync2();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void sync1() throws InterruptedException {

        System.out.println("sync1"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
        Thread.sleep(1000);
        sync2();
        System.out.println("sync1"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public synchronized void sync2() throws InterruptedException {

        System.out.println("sync2"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
        Thread.sleep(1000);
        System.out.println("sync2"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new SyncTest(),"one_thread");  //thread传入的runnable会被执行
        Thread thread1 = new Thread(new SyncTest(),"two_thread");
        thread.start();
        thread1.start();
    }
}

one_threadrun on > 2020-08-09 17:18:48
two_threadrun on > 2020-08-09 17:18:48
sync2two_thread start>>>>2020-08-09 17:18:48 ---------
sync1one_thread start>>>>2020-08-09 17:18:48
sync2one_thread start>>>>2020-08-09 17:18:49
sync2two_thread end >>>>2020-08-09 17:18:49 ---------
sync2one_thread end >>>>2020-08-09 17:18:50
sync1one_thread end >>>>2020-08-09 17:18:50

总结:根据输出结果可以看出,one/two两个线程各跑各的,根本没同步。

综上得出结论:多线程环境下,锁实例方法需要是同一对象。这样synchronized实例方法才能达到同步的效果。

锁静态方法

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/8 17:21
 */
public class SyncTest implements Runnable{

    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void run() {
        try {//调用synchronized实例方法,两次调用的都是对象t
            if ("one_thread".equals(Thread.currentThread().getName())){
                System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
                sync1();
            }else {
                System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
                sync2();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized static void sync1() throws InterruptedException {
        System.out.println("sync1"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
        Thread.sleep(1000);
        sync2();
        System.out.println("sync1"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public synchronized static void sync2() throws InterruptedException {
        System.out.println("sync2"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
        Thread.sleep(1000);
        System.out.println("sync2"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest t = new SyncTest();
        Thread thread = new Thread(t,"one_thread");  //thread传入的runnable会被执行
        Thread thread1 = new Thread(t,"two_thread");
        thread.start();
        thread1.start();

        Thread.sleep(5000);
        System.out.println("######################################");

        Thread one = new Thread(new SyncTest(),"one_thread");  //thread传入的runnable会被执行
        Thread two = new Thread(new SyncTest(),"two_thread");
        one.start();
        two.start();
    }
}


two_threadrun on > 2020-08-09 17:32:29
one_threadrun on > 2020-08-09 17:32:29
sync2two_thread start>>>>2020-08-09 17:32:30
sync2two_thread end >>>>2020-08-09 17:32:31
sync1one_thread start>>>>2020-08-09 17:32:31
sync2one_thread start>>>>2020-08-09 17:32:32
sync2one_thread end >>>>2020-08-09 17:32:33
sync1one_thread end >>>>2020-08-09 17:32:33
######################################
one_threadrun on > 2020-08-09 17:32:34
two_threadrun on > 2020-08-09 17:32:34
sync1one_thread start>>>>2020-08-09 17:32:34
sync2one_thread start>>>>2020-08-09 17:32:35
sync2one_thread end >>>>2020-08-09 17:32:36
sync1one_thread end >>>>2020-08-09 17:32:36
sync2two_thread start>>>>2020-08-09 17:32:36
sync2two_thread end >>>>2020-08-09 17:32:37

总结:上面synchronized都是加到了static静态方法上面,为了方便看,我把相同对象和不同对象调用都写在上面的代码中了,可以看到,无论是相同的对象还是不同的对象,两个线程都是同步执行的。对于sync1调用,可重入性也成立。

锁代码块this

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/8 17:21
 */
public class SyncTest implements Runnable{

    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void run() {
        try {//调用synchronized实例方法,两次调用的都是对象t
            System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
            sync4();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void sync4() throws InterruptedException {
        synchronized (this){
            System.out.println("sync4"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
            Thread.sleep(1000);
            System.out.println("sync4"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
        }
        Thread.sleep(100);   //测试代码块外面的代码同步情况
        System.out.println("sync4 outside >>> "+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));

    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest t = new SyncTest();
        Thread thread = new Thread(t,"one_thread");  //thread传入的runnable会被执行
        Thread thread1 = new Thread(t,"two_thread");
        thread.start();
        thread1.start();

        Thread.sleep(5000);
        System.out.println("######################################");

        Thread one = new Thread(new SyncTest(),"one_thread");  //thread传入的runnable会被执行
        Thread two = new Thread(new SyncTest(),"two_thread");
        one.start();
        two.start();
    }
}

1.one_threadrun on > 2020-08-09 17:46:28
2.two_threadrun on > 2020-08-09 17:46:28
3.sync4one_thread start>>>>2020-08-09 17:46:28
4.sync4one_thread end >>>>2020-08-09 17:46:29
5.sync4two_thread start>>>>2020-08-09 17:46:29
6.sync4 outside >>> one_thread end >>>>2020-08-09 17:46:29
7.sync4two_thread end >>>>2020-08-09 17:46:30
8.sync4 outside >>> two_thread end >>>>2020-08-09 17:46:30
######################################
9.one_threadrun on > 2020-08-09 17:46:33
10.two_threadrun on > 2020-08-09 17:46:33
11.sync4one_thread start>>>>2020-08-09 17:46:33
12.sync4two_thread start>>>>2020-08-09 17:46:33
13.sync4two_thread end >>>>2020-08-09 17:46:34
14.sync4one_thread end >>>>2020-08-09 17:46:34
15.sync4 outside >>> one_thread end >>>>2020-08-09 17:46:34
16.sync4 outside >>> two_thread end >>>>2020-08-09 17:46:34

总结:synchronized锁this代码块跟最开始测试的锁实例方法相比结果是一样的,相同对象可以同步,不同对象同步不了。 区别在于:锁代码块时当前方法除了代码块的代码是非同步的。这样可以灵活使用,提供效率。

锁代码块class

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/8 17:21
 */
public class SyncTest implements Runnable{

    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void run() {
        try {//调用synchronized实例方法,两次调用的都是对象t
            System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
            sync4();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void sync4() throws InterruptedException {
        synchronized (SyncTest.class){
            System.out.println("sync4"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
            Thread.sleep(1000);
            System.out.println("sync4"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
        }
        Thread.sleep(100);
        System.out.println("sync4 outside >>> "+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));

    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest t = new SyncTest();
        Thread thread = new Thread(t,"one_thread");  //thread传入的runnable会被执行
        Thread thread1 = new Thread(t,"two_thread");
        thread.start();
        thread1.start();

        Thread.sleep(5000);
        System.out.println("######################################");

        Thread one = new Thread(new SyncTest(),"one_thread");  //thread传入的runnable会被执行
        Thread two = new Thread(new SyncTest(),"two_thread");
        one.start();
        two.start();
    }
}

two_threadrun on > 2020-08-09 21:23:08
one_threadrun on > 2020-08-09 21:23:08
sync4two_thread start>>>>2020-08-09 21:23:08
sync4two_thread end >>>>2020-08-09 21:23:09
sync4one_thread start>>>>2020-08-09 21:23:09
sync4 outside >>> two_thread end >>>>2020-08-09 21:23:09
sync4one_thread end >>>>2020-08-09 21:23:10
sync4 outside >>> one_thread end >>>>2020-08-09 21:23:10
######################################
one_threadrun on > 2020-08-09 21:23:13
two_threadrun on > 2020-08-09 21:23:13
sync4one_thread start>>>>2020-08-09 21:23:13
sync4one_thread end >>>>2020-08-09 21:23:14
sync4two_thread start>>>>2020-08-09 21:23:14
sync4 outside >>> one_thread end >>>>2020-08-09 21:23:14
sync4two_thread end >>>>2020-08-09 21:23:15
sync4 outside >>> two_thread end >>>>2020-08-09 21:23:15

总结:synchronized锁class代码块的时候跟锁static方法类似,不同对象都能触发同步。缩小代码范围了。

锁实例&&静态

package sync;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @Author zhangyong
 * @Date 2020/8/8 17:21
 */
public class SyncTest implements Runnable{

    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void run() {
        try {//调用synchronized实例方法,两次调用的都是对象t
            System.out.println(Thread.currentThread().getName() + "run on > "+dtf.format(LocalDateTime.now()));
            if ("one_thread".equals(Thread.currentThread().getName())){
                sync4();
            }else{
                sync3();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sync3() throws InterruptedException {
        synchronized (SyncTest.class){
            System.out.println("sync3"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
            Thread.sleep(1000);
            System.out.println("sync3"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
        }
        Thread.sleep(100);
        System.out.println("sync3 outside >>> "+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public void sync4() throws InterruptedException {
        synchronized (this){
            System.out.println("sync4"+Thread.currentThread().getName()+"  start>>>>"+dtf.format(LocalDateTime.now()));
            Thread.sleep(1000);
            System.out.println("sync4"+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
        }
        Thread.sleep(100);
        System.out.println("sync4 outside >>> "+Thread.currentThread().getName()+"  end  >>>>"+dtf.format(LocalDateTime.now()));
    }

    public static void main(String[] args) throws InterruptedException {
        SyncTest t = new SyncTest();
        Thread thread = new Thread(t,"one_thread");  //thread传入的runnable会被执行
        Thread thread1 = new Thread(t,"two_thread");
        thread.start();
        thread1.start();

        Thread.sleep(5000);
        System.out.println("######################################");

        Thread one = new Thread(new SyncTest(),"one_thread");  //thread传入的runnable会被执行
        Thread two = new Thread(new SyncTest(),"two_thread");
        one.start();
        two.start();
    }
}

one_threadrun on > 2020-08-09 21:28:51
two_threadrun on > 2020-08-09 21:28:51
sync4one_thread start>>>>2020-08-09 21:28:51
sync3two_thread start>>>>2020-08-09 21:28:51
sync4one_thread end >>>>2020-08-09 21:28:52
sync3two_thread end >>>>2020-08-09 21:28:52
sync4 outside >>> one_thread end >>>>2020-08-09 21:28:52
sync3 outside >>> two_thread end >>>>2020-08-09 21:28:52
######################################
one_threadrun on > 2020-08-09 21:28:56
two_threadrun on > 2020-08-09 21:28:56
sync4one_thread start>>>>2020-08-09 21:28:56
sync3two_thread start>>>>2020-08-09 21:28:56
sync4one_thread end >>>>2020-08-09 21:28:57
sync3two_thread end >>>>2020-08-09 21:28:57
sync4 outside >>> one_thread end >>>>2020-08-09 21:28:57
sync3 outside >>> two_thread end >>>>2020-08-09 21:28:57

总结:对于锁class和this这种静态and非静态的代码,不同对象和相同对象都是互不干扰的。可以理解为两个锁对象。

详解

  • synchronized用的锁是存在Java对象头里的。
  • Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。
  • monitor object 充当着维护 mutex以及定义 wait/signal API 来管理线程的阻塞和唤醒的角色。
  • Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据和对齐填充,而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制
  • JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
  • 根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

Demo

下面是一个demo,i++是非原子性操作,在执行的时候需要加锁,如下demo所示。

package transfer;

import java.util.concurrent.CountDownLatch;

/**
 * @Author zhangyong
 * @Date 2020/8/14 16:15
 */
public class Add implements Runnable{

    private static int count = 0;   //静态变量多线程操作不安全
    private int instanceCount = 0;  //实例变量多线程操作不安全

    public static void main(String[] args) throws InterruptedException {
        Add a = new Add();
        for (int i = 0; i < 1000; i++) {
            new Thread(a).start();
        }
        Thread.sleep(3000);       //等线程跑完,用CountDownLatch也行,为了不增加复杂度不加了就。
        System.out.println(count+"---"+a.instanceCount);   //998801---998928
    }

    @Override
    public void run() {
//        synchronized (this){    //解开注释,进行同步就是正确的数值1000000了、
            for (int i = 0; i < 1000; i++) {
                count++;
                instanceCount++;
            }
//        }
    }
}

对于静态变量还是成员变量,静态方法还是成员方法都无所谓,不加锁就是多线程不安全的。

参考:
https://segmentfault.com/a/1190000016417017
https://zhuanlan.zhihu.com/p/29866981

10.Lock ReentrantLock ReentrantReadWriteLock?

ReentrantLock是一个可重入的互斥锁,ReentrantLock 类实现了 Lock接口。与synchronized相比可以响应中断并且支持设置成公平锁。还有神奇的 tryLock()

方法作用
ReentrantLock(boolean fair)创建锁实例是否是公平锁,默认非公平锁
int getHoldCount()查询当前线程保持此锁的次数
protected Thread getOwner()查询当前线程保持此锁的次数。
protected Collection getQueuedThreads()返回一个 collection,它包含可能正等待获取此锁的线程。
int getQueueLength()返回正等待获取此锁的线程估计数。
boolean hasQueuedThread(Thread thread)查询给定线程是否正在等待获取此锁。
boolean hasQueuedThreads()查询是否有些线程正在等待获取此锁。
boolean hasWaiters(Condition condition)查询是否有些线程正在等待与此锁有关的给定条件。
boolean isFair()如果是“公平锁”返回true,否则返回false。
boolean isHeldByCurrentThread()查询当前线程是否保持此锁。
boolean isLocked()查询此锁是否由任意线程保持。
void lock()获取锁。
void lockInterruptibly()如果当前线程未被中断,则获取锁。
Condition newCondition())返回用来与此 Lock 实例一起使用的 Condition 实例。
boolean tryLock()仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock(long timeout, TimeUnit unit)如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁
void unlock()释放此锁。

写个demo吧。主要是 lock() unlock() tryLock() lockInterruptibly()的使用

package lock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author zhangyong
 * @Date 2020/8/15 21:03
 */
public class LockTest implements Runnable{
    private static Lock lock = new ReentrantLock(true); //默认是不公平锁

    private int count = 0;

    private AtomicInteger lose = new AtomicInteger(0);

//    @Override             //使用lock.lock();
//    public void run() {
//        for (int i = 0; i < 1000; i++) {
//            try {
//                lock.lock();  //1000000
//                count++;
//            } finally {
//                lock.unlock();
//            }
//        }
//    }

    @Override
    public void run() {
        try {
            if (lock.tryLock(1, TimeUnit.MILLISECONDS)) {  //尝试获取锁,没获取就等待一秒之内获取锁就返回true,否则返回false
                count++;
            }else {
                System.out.println(Thread.currentThread().getName()+" >>>> 获取锁失败");
                lose.addAndGet(1);   //记录失败次数
            }
        } catch (InterruptedException e) {
            //可以在tryLock等待阻塞的时候响应中断
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

//    @Override
//    public void run() {
//        try {
//            //如果当前线程未被中断,则获取锁。
//            // 因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或重入获取。
//            lock.lockInterruptibly();
//            count++;
//        } catch (InterruptedException e) {
//            //可以在tryLock等待阻塞的时候响应中断
//            e.printStackTrace();
//        } finally {
//            lock.unlock();
//        }
//    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(9);
        LockTest test = new LockTest();
        for (int i = 0; i < 10000; i++) {
            es.submit(test);
        }
        Thread.sleep(5000);
        es.shutdown();
        System.out.println(test.count);
        System.out.println(test.lose);

    }
}


pool-1-thread-3 >>>> 获取锁失败
pool-1-thread-8 >>>> 获取锁失败
pool-1-thread-9 >>>> 获取锁失败
pool-1-thread-4 >>>> 获取锁失败
pool-1-thread-5 >>>> 获取锁失败
pool-1-thread-2 >>>> 获取锁失败
pool-1-thread-7 >>>> 获取锁失败
pool-1-thread-6 >>>> 获取锁失败
9992
8

ReentrantReadWriteLock Demo

读写锁适合读多写少的场景,比互斥锁要快。

  • 写锁会产生互斥,任何读写操作需要等待写锁释放才能进行
  • 读可以多线程共享读。 共享锁(读) 排它锁(写)
  • 读写锁是可重入的,可重入特性还允许从写锁降级到读锁—通过获取写锁,然后获取读锁,然后释放写锁。
  • 对于源码而言,读写锁ReentrantReadWriteLock.ReadLock 实现Lock接口,与Lock玩法类似,但是由AQS实现。
package lock;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Author zhangyong
 * @Date 2020/8/16 10:15
 */
public class ReadWriteTest {

    private final Map<String,String> map = new HashMap<>();

    private final static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    //非公平锁比公平锁快
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);

    //干活的读写锁  Lock的实现类
    private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    //读锁
    public String get(String key){
        try {
            readLock.lock();
            Thread.sleep(1000);
            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" >>>> 准备读");
            return map.get(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    //写锁
    public void set(String key,String value){
        try {
            writeLock.lock();
            Thread.sleep(3000);
            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" >>>> 准备写");
            map.put(key,value);
            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" >>>> 写完");
        } catch (InterruptedException e) {
            System.out.println(dtf.format(LocalDateTime.now())+": "+"sleep interrupted...");
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

//    //互斥锁读
//    public synchronized String get(String key){
//        try {
//            Thread.sleep(1000);
//            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" >>>> 准备读");
//            return map.get(key);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//            return map.get(key);
//        }
//    }
//
//    //互斥锁写
//    public synchronized void set(String key,String value){
//        try {
//            Thread.sleep(3000);
//            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" >>>> 准备写");
//            map.put(key,value);
//            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" >>>> 写完");
//        } catch (InterruptedException e) {
//            System.out.println(dtf.format(LocalDateTime.now())+": "+"sleep interrupted...");
//            e.printStackTrace();
//        }
//    }
    public static void main(String[] args) throws InterruptedException {
        ReadWriteTest test = new ReadWriteTest();
        ExecutorService es = Executors.newFixedThreadPool(5);
        System.out.println(dtf.format(LocalDateTime.now())+": 开始程序...");
        Runnable write = new Runnable() {
            @Override
            public void run() {
                test.set("zy","zhangyong");
            }
        };
        Runnable read = new Runnable() {
            @Override
            public void run() {
                String value = test.get("zy");
                System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" >>>> "+value);
            }
        };
        for (int i = 0; i < 2; i++) {
            es.submit(read);
        }
        Thread.sleep(100); //此处sleep 是为了前面线程在调用中
        es.submit(write);
        Thread.sleep(100); //此处sleep 是为了前面线程在调用中
        es.submit(read);
        es.shutdown();
    }
}

//2020-08-16 14:27:05: 开始程序…
//2020-08-16 14:27:06: pool-1-thread-2 >>>> 准备读
//2020-08-16 14:27:06: pool-1-thread-2 >>>> null
//2020-08-16 14:27:07: pool-1-thread-4 >>>> 准备读
//2020-08-16 14:27:07: pool-1-thread-4 >>>> null
//2020-08-16 14:27:10: pool-1-thread-3 >>>> 准备写
//2020-08-16 14:27:10: pool-1-thread-3 >>>> 写完
//2020-08-16 14:27:11: pool-1-thread-1 >>>> 准备读
//2020-08-16 14:27:11: pool-1-thread-1 >>>> zhangyong
//可以看到读和写都互斥的,锁被占用就得等待释放,从头到尾需要6s

//2020-08-16 14:28:24: 开始程序…
//2020-08-16 14:28:25: pool-1-thread-1 >>>> 准备读
//2020-08-16 14:28:25: pool-1-thread-2 >>>> 准备读
//2020-08-16 14:28:25: pool-1-thread-2 >>>> null
//2020-08-16 14:28:25: pool-1-thread-1 >>>> null
//2020-08-16 14:28:28: pool-1-thread-3 >>>> 准备写
//2020-08-16 14:28:28: pool-1-thread-3 >>>> 写完
//2020-08-16 14:28:29: pool-1-thread-4 >>>> 准备读
//2020-08-16 14:28:29: pool-1-thread-4 >>>> zhangyong
//使用读写锁,当不发生写的时候,读不互斥,从头到尾需要5s

11.servlet controller 为什么非线程安全

servlet controller 是单例模式的,但是每个请求都是独立的(线程),请求过来用同一个servlet/controller实例来处理,这样能保证内存占用少,不创建过多对象,降低垃圾回收的时间。
但是servlet和controller类中定义的实例变量如果被多线程操作就有不安全的问题。可能造成数据错乱。

解决办法:

  • 使用多例模式 @Scope(“prototype”) controller引用的 service也要这么加注解
  • 使用ThreadLocal,ThreadLocal每个线程会有一个变量副本,相当于每个线程都有一份变量,互相不影响所以多线程安全。但是线程如果不销毁会导致轻微的内存泄漏。

12.volatile

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题(数据一致性),有序性问题。

volatile 解决了 可见性和有序性,使用内存屏障保证了数据读写时的可见性(一致性)以及禁止指令重排序,将声明为volatile的共享变量的前面代码和后面代码分开不让跨过共享变量进行重排序,但是volatile不能解决原子性,原子性用锁来解决吧。

可见性:

  • 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。(注意修改才会重新读,如果线程1只读取没发生修改,那么线程2读的还是之前内存的值,线程2执行完,线程1再执行就有问题了,这个也是可见性和原子性的区别) i++

  • 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

重排序:

  • 不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  • 它会强制将对缓存的修改操作立即写入主存;

  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

下面的链接是几篇比较不错的文章,读完 JMM volitle就明白了

内存屏障:https://www.jianshu.com/p/2ab5e3d7e510

内存模型:https://zhuanlan.zhihu.com/p/29881777

Volatile:https://www.cnblogs.com/dolphin0520/p/3920373.html

13.condition && semaphore

  • condition 对标synchronized中的wait/notify
  • semaphore 相当于一个有指定值数量的资源,资源全部被占用才开始阻塞,就像买车票的窗口。
  • condition 可以表达多个wait/notify ,更加全面,例如生产者消费者的阻塞队列。
  • condition 和 semaphore都是基于AQS的
  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。

Condition Demo

package lock;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author zhangyong
 * @Date 2020/8/17 15:03
 */
public class ConditionTest {

    private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private final Integer[] clothes;

    private static int count = 0;

    private final Lock lock = new ReentrantLock();
    private final Condition producer = lock.newCondition();
    private final Condition consumer = lock.newCondition();

    public ConditionTest(int size) {
        this.clothes = new Integer[size];
    }

    public void push(){
        try {
            lock.lock();
            while (count == clothes.length){
                //满了,等待出库(消费)
                producer.await();
            }
            //入库(生产)
            Thread.sleep(new Random().nextInt(1000));
            count++;
            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" 生产完毕!!,剩余库存:"+count);
            //通知出库(消费)
            consumer.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void pop(){
        try {
            lock.lock();
            while (count == 0){
                //没了,等待生产
                consumer.await();
            }
            //消费
            Thread.sleep(new Random().nextInt(1000));
            count--;
            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" 消费完毕,剩余库存:"+count);
            //通知生产
            producer.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void full(){
        try {
            lock.lock();
            //生产满
            Thread.sleep(new Random().nextInt(1000));
            count = clothes.length;
            System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+" 全部生产完毕,剩余库存:"+count);
            //通知所有阻塞的消费者消费
            consumer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionTest conditionTest = new ConditionTest(10);
        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 6; i++) {
            es.submit(new Runnable() {
                @Override
                public void run() {
                    conditionTest.push();
                }
            });
        }
        for (int i = 0; i < 10; i++) {
            es.submit(new Runnable() {
                @Override
                public void run() {
                    conditionTest.pop();
                }
            });
        }
        Thread.sleep(10000);
        es.submit(new Runnable() {   //在某一刻将库存灌满,供阻塞的消费者消费
            @Override
            public void run() {
                conditionTest.full();
            }
        });
        es.shutdown();
        //2020-08-17 20:53:55: pool-1-thread-1 生产完毕!!,剩余库存:1
        //2020-08-17 20:53:55: pool-1-thread-2 生产完毕!!,剩余库存:2
        //2020-08-17 20:53:56: pool-1-thread-2 消费完毕,剩余库存:1
        //2020-08-17 20:53:57: pool-1-thread-2 消费完毕,剩余库存:0
        //2020-08-17 20:53:58: pool-1-thread-5 生产完毕!!,剩余库存:1
        //2020-08-17 20:53:58: pool-1-thread-5 消费完毕,剩余库存:0
        //2020-08-17 20:53:59: pool-1-thread-4 生产完毕!!,剩余库存:1
        //2020-08-17 20:54:00: pool-1-thread-3 生产完毕!!,剩余库存:2
        //2020-08-17 20:54:01: pool-1-thread-3 消费完毕,剩余库存:1
        //2020-08-17 20:54:01: pool-1-thread-3 消费完毕,剩余库存:0
        //2020-08-17 20:54:02: pool-1-thread-1 生产完毕!!,剩余库存:1
        //2020-08-17 20:54:02: pool-1-thread-1 消费完毕,剩余库存:0
        //2020-08-17 20:54:05: pool-1-thread-1 全部生产完毕,剩余库存:10
        //2020-08-17 20:54:06: pool-1-thread-2 消费完毕,剩余库存:9
        //2020-08-17 20:54:07: pool-1-thread-5 消费完毕,剩余库存:8
        //2020-08-17 20:54:08: pool-1-thread-4 消费完毕,剩余库存:7
        //2020-08-17 20:54:08: pool-1-thread-3 消费完毕,剩余库存:6
    }
}

Semaphore Demo

package lock;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.concurrent.Semaphore;

/**
 * @Author zhangyong
 * @Date 2020/8/17 19:22
 */
public class SemaphoreTest {

    private final static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(new Random().nextInt(1000));
                        semaphore.acquire();
                        System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+"获取共享资源");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println(dtf.format(LocalDateTime.now())+": "+Thread.currentThread().getName()+"释放!!!");
                        semaphore.release();
                    }

                }
            }).start();
        }
        //2020-08-17 20:12:33: Thread-7获取共享资源
        //2020-08-17 20:12:33: Thread-2获取共享资源
        //2020-08-17 20:12:34: Thread-0获取共享资源
        //2020-08-17 20:12:35: Thread-2释放!!!
        //2020-08-17 20:12:35: Thread-7释放!!!
        //2020-08-17 20:12:35: Thread-1获取共享资源
        //2020-08-17 20:12:35: Thread-6获取共享资源
        //2020-08-17 20:12:35: Thread-0释放!!!
        //2020-08-17 20:12:35: Thread-9获取共享资源
        //2020-08-17 20:12:36: Thread-6释放!!!
        //2020-08-17 20:12:36: Thread-1释放!!!
        //2020-08-17 20:12:36: Thread-5获取共享资源
        //2020-08-17 20:12:36: Thread-3获取共享资源
        //2020-08-17 20:12:36: Thread-9释放!!!
        //2020-08-17 20:12:36: Thread-4获取共享资源
        //2020-08-17 20:12:37: Thread-5释放!!!
        //2020-08-17 20:12:37: Thread-3释放!!!
        //2020-08-17 20:12:37: Thread-8获取共享资源
        //2020-08-17 20:12:37: Thread-4释放!!!
        //2020-08-17 20:12:38: Thread-8释放!!!
        //最多3个线程同时获取   像go里面一个channel缓冲为3的通道,超过设置的3才开始阻塞
    }
}

14.AQS原理源码

  1. AQS是一个框架性质的东西,许多锁都依赖于它,底层的同步机制是通过CAS。使用模板方法设计模式,将公共代码写好,开发者如果想做个锁只需要实现这个AQS抽象类,重写部分protected方法即可。非常方便。
  2. AQS使用volatile int state 来维护共享资源(数量)以及一个FIFO的线程等待队列CLH
  3. AQS分为两种资源共享方式,共享锁和排它锁(独占),不解释了。
  4. AQS的实现只需要实现共享资源state的获取和释放。实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。

从独占锁开说,具体的实现大体是:

public final void acquire(int arg) {    //架子代码,最开始调的是这个
	//if判断是false那么说明可以获取资源。因为判断过程中,如果没有资源可用会被阻塞。直到资源可用或中断返回
        if (!this.tryAcquire(arg) && this.acquireQueued(this.addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)) {
        	//如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
            selfInterrupt();
        }

    }
  • tryAcquire(arg) (需要自己重写的方法)获取资源,获取到返回,获取不到就进入等待队列(如果资源被锁定了,再来线程是进到CLH队列的,但是进入前会尝试获取一下共享资源,这样不能保证是先来的线程先获取,可能被后来者插队,因此也就默认不是公平锁了。对于公平锁就往队列后面排队)具体要看怎么实现了。可以参考ReentrantLock的实现
  • addWaiter将node节点加入队尾
  • acquireQueued()使线程“阻塞”在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。 这个队列是先进先出的,除非线程状态不对(被取消等)会被踢出队列,后者连上来。
  • 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
protected boolean tryAcquire(int arg) {    //AQS
        throw new UnsupportedOperationException();
    }
//将node加入队尾,也是基于CAS
private AbstractQueuedSynchronizer.Node addWaiter(AbstractQueuedSynchronizer.Node mode) {
        AbstractQueuedSynchronizer.Node node = new AbstractQueuedSynchronizer.Node(mode);

        AbstractQueuedSynchronizer.Node oldTail;
        do {
            while(true) {
                oldTail = this.tail;
                if (oldTail != null) {
                    node.setPrevRelaxed(oldTail);
                    break;
                }

                this.initializeSyncQueue();
            }
        } while(!this.compareAndSetTail(oldTail, node));

        oldTail.next = node;
        return node;
    }
//阻塞获取资源,获取到返回false,被中断返回true 可以获取资源的是老二,除非老二状态不对,那么老三上...
final boolean acquireQueued(AbstractQueuedSynchronizer.Node node, int arg) {
        boolean interrupted = false;

        try {
            while(true) {
                AbstractQueuedSynchronizer.Node p = node.predecessor();
                //如果当前是头结点的下一个(老二),那么尝试获取资源
                if (p == this.head && this.tryAcquire(arg)) {
                    this.setHead(node);
                    p.next = null;
                    return interrupted;
                }

                if (shouldParkAfterFailedAcquire(p, node)) {
                    interrupted |= this.parkAndCheckInterrupt();
                }
            }
        } catch (Throwable var5) {
            this.cancelAcquire(node);
            if (interrupted) {
                selfInterrupt();
            }

            throw var5;
        }
    }
//看状态是否满足signal   -1 如果不满足向上遍历,将标记废弃的线程的踢出队列。
private static boolean shouldParkAfterFailedAcquire(AbstractQueuedSynchronizer.Node pred, AbstractQueuedSynchronizer.Node node) {
        int ws = pred.waitStatus;
        if (ws == -1) {
            return true;
        } else {
            if (ws > 0) {
                do {
                    node.prev = pred = pred.prev;
                } while(pred.waitStatus > 0);

                pred.next = node;
            } else {
                pred.compareAndSetWaitStatus(ws, -1);
            }

            return false;
        }
    }

对于释放差不多,维护好state。以countLatchDown为例

public final boolean releaseShared(int arg) {
        if (this.tryReleaseShared(arg)) {
            this.doReleaseShared();
            return true;
        } else {
            return false;
        }
    }

protected boolean tryReleaseShared(int releases) {
            int c;
            int nextc;
            do {
                c = this.getState();
                if (c == 0) {
                    return false;
                }

                nextc = c - 1;
            } while(!this.compareAndSetState(c, nextc));

            return nextc == 0;
        }

private void doReleaseShared() {
        while(true) {
            AbstractQueuedSynchronizer.Node h = this.head;
            if (h != null && h != this.tail) {
                int ws = h.waitStatus;
                if (ws == -1) {
                    if (!h.compareAndSetWaitStatus(-1, 0)) {
                        continue;
                    }

                    this.unparkSuccessor(h);
                } else if (ws == 0 && !h.compareAndSetWaitStatus(0, -3)) {
                    continue;
                }
            }

            if (h == this.head) {
                return;
            }
        }
    }

https://www.cnblogs.com/waterystone/p/4920797.html

15.threadlocal 用法场景及其内存泄漏

ThreadLocal提供线程内部的局部变量,在多线程环境下访问能保证各个线程的变量都是独立存在的,相当于一个副本。— 因为 Thread.currentThread 将变量放到当前线程来实现的。

  • 每个线程都拥有这个变量,多线程之间不互相影响。
  • 一个线程不同地方调用也比较方便。避免传参。

关于内存泄漏:

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。

非线程池不会造成内存泄漏,用完调用remove方法即可解决。

package sync;

import java.util.concurrent.*;

/**
 * @Author zhangyong
 * @Date 2020/8/14 20:02
 */
public class ThreadLocalTest {

    private static ExecutorService es = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ConcurrentHashMap<Integer,String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 10; i++) {
            Future<Integer> future = es.submit(new ThreadTest());
            map.put(future.get(),"");
        }
        Thread.sleep(1000);
        map.forEach((k,v)->{
            System.out.println(k);  //1   输出全是1证明每个线程都不一样,即使是线程池,也不会累加
        });
        es.shutdown();
    }
}

class ThreadTest implements Callable<Integer>{
//    public ThreadLocal<SimpleDateFormat> s = new ThreadLocal<>(){
//        @Override
//        protected SimpleDateFormat initialValue() {  //这个是ThreadLocal 的方法,初始化泛型 T 用的。
//            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        }
//    };

    private ThreadLocal<Integer> s = new ThreadLocal<>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    @Override
    public Integer call() throws Exception {
        s.set((s.get())+1);
        System.out.println(s.get());
        return s.get();
    }
}

对于源码也没啥好看的,他实际上操作的就是 Thread.currentThread();

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = this.getMap(t);
        if (map != null) {
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = e.value;
                return result;
            }
        }

        return this.setInitialValue();
    }
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = this.getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            this.createMap(t, value);
        }

    }

16.threadpoolExecutor? 线程池

ThreadpoolExecutor就是Java里面线程池的实现。包括Executor fork都是以这个为基准的。下面看一下构造方法

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
        this.mainLock = new ReentrantLock();
        this.workers = new HashSet();
        this.termination = this.mainLock.newCondition();
        if (corePoolSize >= 0 && maximumPoolSize > 0 && maximumPoolSize >= corePoolSize && keepAliveTime >= 0L) {
            if (workQueue != null && threadFactory != null && handler != null) {
                this.corePoolSize = corePoolSize;
                this.maximumPoolSize = maximumPoolSize;
                this.workQueue = workQueue;
                this.keepAliveTime = unit.toNanos(keepAliveTime);
                this.threadFactory = threadFactory;
                this.handler = handler;
            } else {
                throw new NullPointerException();
            }
        } else {
            throw new IllegalArgumentException();
        }
    }

构造方法中的字段含义如下:

  • corePoolSize:核心线程数量,当有新任务在execute()方法提交时,会执行以下判断:

    • 如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
    • 如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
    • 如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
    • 如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;
      所以,任务提交时,判断的顺序为 corePoolSize –> workQueue –> maximumPoolSize。
  • maximumPoolSize:最大线程数量;

  • workQueue:等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列;

  • workQueue:保存等待执行的任务的阻塞队列,当提交一个新的任务到线程池以后, 线程池会根据当前线程池中正在运行着的线程的数量来决定对该任务的处理方式,主要有以下几种处理方式:

    • 直接切换:这种方式常用的队列是SynchronousQueue,但现在还没有研究过该队列,这里暂时还没法介绍;
    • 使用无界队列:一般使用基于链表的阻塞队列LinkedBlockingQueue。如果使用这种方式,那么线程池中能够创建的最大线程数就是corePoolSize,而maximumPoolSize就不会起作用了(后面也会说到)。当线程池中所有的核心线程都是RUNNING状态时,这时一个新的任务提交就会放入等待队列中。
    • 使用有界队列:一般使用ArrayBlockingQueue。使用该方式可以将线程池的最大线程数量限制为maximumPoolSize,这样能够降低资源的消耗,但同时这种方式也使得线程池对线程的调度变得更困难,因为线程池和队列的容量都是有限的值,所以要想使线程池处理任务的吞吐率达到一个相对合理的范围,又想使线程调度相对简单,并且还要尽可能的降低线程池对资源的消耗,就需要合理的设置这两个数量。
      • 如果要想降低系统资源的消耗(包括CPU的使用率,操作系统资源的消耗,上下文环境切换的开销等), 可以设置较大的队列容量和较小的线程池容量, 但这样也会降低线程处理任务的吞吐量。
      • 如果提交的任务经常发生阻塞,那么可以考虑通过调用 setMaximumPoolSize() 方法来重新设定线程池的容量。
      • 如果队列的容量设置的较小,通常需要将线程池的容量设置大一点,这样CPU的使用率会相对的高一些。但如果线程池的容量设置的过大,则在提交的任务数量太多的情况下,并发量会增加,那么线程之间的调度就是一个要考虑的问题,因为这样反而有可能降低处理任务的吞吐量。
  • keepAliveTime:线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;

  • threadFactory:它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

  • handler:它是RejectedExecutionHandler类型的变量,表示线程池的饱和策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。线程池提供了4种策略:

    • AbortPolicy:直接抛出异常,这是默认策略;
    • CallerRunsPolicy:用调用者所在的线程来执行任务;
    • DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    • DiscardPolicy:直接丢弃任务;

ExecutorService es = Executors.newFixedThreadPool(10);

ExecutorService默认是LinkedBlockingQueue 无界队列,使用拒绝策略AbortPolicy

关于那个 ThreadFactory就是一个接口,里面有一个newThread方法,用来创建线程的。可以给线程一些自定义操作。

更详细可以看下下面这篇文章,本段摘抄自下面引用、

http://www.ideabuffer.cn/2017/04/04/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9AThreadPoolExecutor

17.concurrentHashMap源码(TODO)

//TODO
concurrentHashMap是多线程安全的,在JDK7版本及之前是使用分段锁segement,一共持有16个锁,每个锁数据访问互不影响。这样比HashTable的全部synchronized要快16倍。但是还可以再优化。也就是JDK8版本的CAS+红黑树。
HashMap在jdk8的时候使用了红黑树,最开始是一个Array 哈希冲突变成链表,链表长度超过8变成红黑树。这样遍历链表的(n)变成了 log2(n) 快了很多。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java面试题复习整理(多线程) 的相关文章

  • Android 4.2 SafeVolume机制

    最近一个项目过认证 xff0c 在声压测试时failed 整改方案为 xff1a 在用户将耳机音量提高至安全音量以上时 xff0c 阻止此操作并弹出警告框 xff0c 待用户确认后才提升音量 一开始并不知道android4 2中默认自带了这
  • 命令行查看android手机wi-fi密码

    两招帮你查看wifi密码 xff08 抱歉 xff1a 由于无法传第三张图片 xff0c 第三个图片内容请参照参考网址获得 xff09 第一 xff0c 手机必须root 第二 xff0c 用es文件浏览器或RE管理器进入date misc
  • android网络时间同步总结

    本文转自 xff1a http www cnblogs com hoji real archive 2011 11 14 2247984 html 最近看了下网络时间同步 xff0c 总结一下 整体描述 xff1a android网络时间同
  • win7删除ubuntu系统

    win7 43 ubuntu双系统 xff0c ubuntu开机的时候 xff0c 电脑会响 xff0c ubuntu系统进不去 进入win7系统后 xff0c F盘是通过磁盘管理压缩剩余空间安装ubuntu系统的 xff0c QQ安装在F
  • 手机电池和taskId的寻找

    刷机的时候启动手机时间比较久 xff0c 拔掉电池给手机断电 xff0c 启动的比较快一点 一直这样干 xff0c 一段时间以后 xff0c 手机充电的时候 xff0c 会显示bad battery 提示电池坏掉 电池坏掉后 xff0c 刷
  • 如何使用Proteus进行电路设计仿真?

    Proteus是一款功能非常强大的软件 xff0c 是英国著名的EDA工具 仿真软件 xff0c 从原理图布图 代码调试到单片机与外围电路协同仿真 xff0c 一键切换到PCB设计 xff0c 真正实现了从概念到产品的完整设计 支持和Kei
  • OKHttpUtils使用介绍

    一 xff0c 概述 在上一篇blog的末尾讲到了OKHttp使用时的缺点 xff0c 和对OKHttp封装的必要性 在github上有很多对OKHttp封装的优秀框架 xff0c 其首推的就是hongyang大神的OKHttpUtils
  • Ubuntu18.04LTS系统盘制作

    记录一下制作系统盘的过程 xff0c 参考资料如下网址 xff0c 谢谢 win10下安装Ubuntu16 04双系统 xff0c 用软碟通制作系统盘 gt 点击此处网址 xff1b 安装win7 Ubuntu16 04双系统 xff0c
  • vmware12-15中ubuntu15.10-18.10的vmwaretools失效,不能拖动复制粘贴以及自动适应窗口分辨率

    新安装或异常关机或重新划分分区导致的vmware tools失效 xff0c 不能拖动复制粘贴文件文本以及自动适应窗口分辨率 xff0c 无论怎样重装vmware tools或open vm tools均无效 最后发现有效的方法如下 xff
  • 【环境搭建】Docker镜像相关操作(切换镜像源、查询、获取、查看、创建、上传、保存、删除等)

    目录 1 镜像源查看及设置2 镜像相关操作2 1 获取镜像列表2 2 镜像下载2 3 查看本地的镜像2 4 从镜像创建容器2 5 将容器抽象为镜像 commit2 6 将容器抽象为镜像 Dockerfile2 7 将镜像保存为压缩包2 8
  • 【废了-准备删除02】信息收集——基于WAMP的drupal7.x管理系统

    目录 1 概述2 域名 子域名 IP信息收集3 端口扫描3 1 扫描过程3 2 小结 4 网站目录扫描4 1 目的4 2 dirbuster 扫描4 3 御剑后台扫描4 4 小结 5 指纹识别5 1 目的5 2 指纹识别5 3 指纹利用5
  • Spring boot App启动报错 missing ServletWebServerFactory bean

    将一个普通Java App应用改写为Java Web App xff0c 添加了spring boot starter parent之后 xff0c Run as Spring App一致报如下错 org springframework c
  • 开源项目|RT-Thread 软件包应用作品:水墨屏桌面台历

    简介 平时经常会有一些事情忘记 xff0c 比如今天几号 xff0c 星期几 xff0c 哪天有什么事情要做 有时候写在本子上 xff0c 有时候记在微信里 xff0c 但有时候连记在哪里都忘记了 为了应对这个情况 xff0c 我制作了一款
  • 【嵌入式AI入门日记】将 AI 模型移植到 RT-Thread 上(1)

    本期我们分享主题是如何将 AI 模型部署到嵌入式系统中 xff0c 下一期将介绍如何在 RT Thread 操作系统上运行 Mnist Demo xff08 手写数字识别 xff09 嵌入式关联 AI AI落地一直是一个很红火的前景和朝阳行
  • uc/os-ii任务调度的锁定与解锁

    调度器上锁函数OSSchedlock 的功能是用于禁止任务调度 xff0c 使任务保持对CPU的控制权 调度器开锁函数OSSchedUnlock 的功能是解除对任务调度的禁止 调度器上锁和开锁的实现原理是 xff1a 对全局变量锁定嵌套计数
  • uc/os-ii信号量集

    在实际应用中 xff0c 任务常常需要与多个事件同步 xff0c 即要根据多个信号量组合作用的结果来决定任务的运行方式 C OS II为了实现多个信号量组合的功能定义了一种特殊的数据结构 信号量集 信号量集所能管理的信号量都是一些二值信号
  • 【OK6410裸机程序】点亮LED

    globl start start 硬件相关的设置 Peri port setup ldr r0 61 0x70000000 orr r0 r0 0x13 mcr p15 0 r0 c15 c2 4 64 256M 0x70000000 0
  • 通过串口实现printf和scanf函数

    转自 草根老师博客 xff08 程姚根 xff09 在做裸板开发时 xff0c 常常需要通过输出或者通过串口输入一些信息 在有操作系统机器上 xff0c 我们很少关心输入和输出的问题 因为有很多现成的库函数供我们调用 在做裸板开发时 xff
  • DDR协议解析

    DRAM内部分割成多个L Bank xff0c 每个L Bank形状相同 xff0c 彼此独立 xff0c 可以独立工作 早期的DRAM芯片内部分为2个L Bank xff0c 后来是4个 xff0c DDR3内存芯片为8个 在进行寻址时需
  • apt-get安装指定版本&查询版本

    一 通过apt get安装指定版本 apt get install lt lt package name gt gt 61 lt lt version gt gt 二 查询指定软件有多少个版本 说明 xff1a 在Linux用这个查询并不能

随机推荐

  • 使用apt-get install时有时候一堆依赖要安装,一个一个安装特别烦人,可以直接用suggest全部安装,具体命令如下

    使用apt get install时有时候一堆依赖要安装 xff0c 一个一个安装特别烦人 xff0c 可以直接用suggest全部安装 xff0c 具体命令如下 apt get install install suggests packa
  • linux-Centos 7下tftp-server服务的安装与配置

    转自 http www cnblogs com 5201351 p 4934625 html TFTP xff08 Trivial File Transfer Protocol 简单文件传输协议 xff09 是TCP IP协议族中的一个用来
  • Linux启动打印信息

    U Boot 1 1 6 Oct 5 2016 16 45 02 for SMDK6410 u boot 1 1 6 Updated for OK6410 TE6410 Board Version 2012 09 23 OEM Forlin
  • 对比S3C6410外部中断STM32外部中断

    转自 xff1a http comm chinaaet com adi blogdetail aspx id 61 40071 amp currentpage 61 2 a S3C6410外部中断 中断在嵌入式里面是很常见的一个功能了 通过
  • shell脚本记录

    1 find name o 找出当前目录下所有的 o文件 使用在makefile中如下 clean rm f liblog so 96 find name o 96
  • makefile中的patsubst

    1 wildcard 扩展通配符 2 notdir xff1a 去除路径 3 patsubst xff1a 替换通配符 例子 xff1a 建立一个测试目录 xff0c 在测试目录下建立一个名为sub的子目录 mkdir test cd te
  • ElasticSearch学习&&理解

    注 xff1a 本篇的es基于7 5 1版本 目录 Elasticsearch是什么 xff1f ElasticSearch的环境搭建 ElasticSearch的名词 ElasticSearch查询出的数据格式 ElasticSearch
  • Kibana学习&理解

    注 xff1a 本篇的kibana基于7 5 1版本 Kibana是什么 xff1f kibana是一个数据可视化平台 展示与分析 将es里面的东西通过各种图表展示出来 xff0c 还可以执行es的各种搜索 amp 监控 Kibana环境搭
  • filebeat学习

    注 xff1a 本篇基于filebeat7 5 2 filebeat是什么 xff1f Filebeat 是用于转发和集中日志数据的轻量级传送程序 作为服务器上的代理安装 xff0c Filebeat 监视您指定的日志文件或位置 xff0c
  • Git Flow 用法

    git flow 工作流程 如下图所示 master 分支 master 分支主要方稳定 随时可上线的版本 这个分支只能从别的分支上合并过来 xff0c 一般来讲 xff0c 从develop 上合并 xff0c 或者从hotfix分支上合
  • Qt父窗口与子窗口间的焦点传递问题的完美解决

    使用activateWindow 或者raise 参考文章 xff1a https blog csdn net Hoarce article details 107215868 http www manongjc com detail 19
  • Git 工作中的一些命令操作

    本篇为工作中 git 使用过程中的一些操作记载 xff0c 不定期更新 目录 1 git 推本地代码到远程 2 git 放弃修改 commit 撤销远程提交记录 3 git pull push fetch 4 git关联本地与远程分支 5
  • php如何使用S3

    本篇是新手使用PHP调aws的s3服务的一些心得 一 关于AWS S3 s3是一个文件存储服务 xff0c 当需要做成服务来进行微服务调用 xff0c 或者终端服务端文件交流使用s3是一个非常不错的选择 aws各种常见的语言例如 xff1a
  • MySQL相关面试题

    1 MySQL text长度 mysql的text是65535的字节限制 xff0c 而pg是不限制的 2 覆盖索引 聚簇索引 xff08 https blog csdn net alexdamiao article details 519
  • Redis相关面试题

    1 缓存是什么 xff1f 缓存分为本地缓存和分布式缓存 以Java为例 xff0c guava实现的就是本地缓存 xff0c 生命周期随JVM销毁而结束 起多个服务实例 xff0c 就有多份缓存 xff0c 不具有一致性 redis和me
  • 如何断网安装docker

    docker rpm安装 不能联网情况 生产环境可能是不能联网的 xff0c 当我们需要用到docker 或其他组件 的时候 xff0c 就需要借助能联网的环境下载好rpm包 xff0c 然后去操作系统服务器装下载好的docker RPM包
  • docker相关

    优势 xff1a 1 启动快 传统的虚拟机技术启动应用服务往往需要数分钟 xff0c 而 Docker 容器应用 xff0c 由于直接运行于宿主内核 xff0c 无需启动完整的操作系统 xff0c 因此可以做到秒级 甚至毫秒级的启动时间 大
  • Liunx下源代码安装&&make&&makefile

    Linux下安装软件的方式分为源代码安装和二进制安装 源代码安装 xff0c 即使用应用程序源代码进行编译安装二进制安装 xff0c 例如red hat发行的 rpm包 debian发行的 deb包 源代码安装 用c语言为例 include
  • Linux下rpm&yum&apt-get

    RPM简介 RPM命名 RedHat Package Manager xff0c 简称则为RPM 属于Red Hat阵营的 xff0c 与其并列的则是debian centos中大部分我们安装都是使用yum install xff0c 而d
  • Java面试题复习整理(多线程)

    文章目录 1 aio nio bio epoll select2 reactor模式介绍Reactor软件工程java代码总结 3 Java中的cas乐观锁4 自旋锁是什么 xff1f 自旋锁 amp amp 自适应自旋锁 amp amp