java多线程(1)

283 阅读3分钟

1.1多线程基础知识:

临界资源和临界区:

线程之间的公共资源,临界资源是指一次只允许一个线程使用的资源,

临界区值指的是访问临界资源的那段代码,每次临界区只允许一个线程进入

死锁:

死锁是多个线程互相持有对方需要的资源,导致无限期等待的一种情况。

如何实现一个新线程:

实现runnable 接口 和 继承Thread 类

屏幕快照 2021-12-23 下午11.35.26.png

实际上thread的代码也实现了runnable接口 屏幕快照 2021-12-23 上午12.04.05.png

thread和runnable的区别

  1. java不允许多继承,所以继承了thread以后就不能继承别的类,但是实现runnable接口以后还是可以继续实现其他的接口的。
  2. 没有了(网上说的数据共享原因其实是不正确的 这里我实现了网上的卖票代码,然后把继承thread类 和runnable接口的实现类改成一样的方法:
static class MyThread2 implements Runnable {
    private int ticket = 5;

    public void run() {
        while (true) {
            System.out.println("Runnable ticket = " + ticket--);
            if (ticket < 0) {
                break;
            }
        }
    }
}


static class MyThread extends Thread {
    private int ticket = 5;

    public void run() {
        while (true) {
            System.out.println("Thread ticket = " + ticket--);
            if (ticket < 0) {
                break;
            }
        }
    }
}

public static void main(String[] args) {
    MyThread2 mt = new MyThread2();
    new Thread(mt).start();
    new Thread(mt).start();
    
    MyThread myThread = new MyThread();
    new Thread(myThread).start();
    new Thread(myThread).start();

运行结果如下

屏幕快照 2021-12-23 下午11.48.31.png 两种实现方式完全一致;实际上这两个就是完全没有任何区别,只是不同的实现方法,如果真的有人去问你这两个的差距,那肯定是他自己并没有认真阅读这两个类的源码。

thread为什么不能用run实现

我们都知道thread 里面实现新线程用的是start() 而不是run(),实际上如果使用run()方法来实现新线程的话,实现的逻辑还是进行顺序执行的。但是使用start()的话,就可以看到真正新线程导致的随机结果

屏幕快照 2021-12-23 下午11.59.00.png

这里看到start方法的源码

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

start0()方法是start的核心实现方法,这个方法是一个native修饰的方法,使用了Native的方法会进入本地方法栈,然后调用本地方法接口JNI(java Native Interface,JNI是什么呢,这里我找了一个网图来解释一下:

image.png 一般来说JNI的调用有这么一个过程:Java Code -> JNI -> C/C++ Code 实际上就是调用了jvm中的本地方法栈:

image.png

回到我们thread的start()方法,问题是:本地线程执行的应该是本地代码,而 Java 线程提供的线程函数run()是 Java 方法,编译出的是 Java 字节码。

所以, Java 线程其实提供了一个统一的线程函数,该线程函数通过 Java 虚拟机调用 Java 线程方法 , 这是通过 Java 本地方法 start0 调用来实现的。

也就是新创建的线程启动调用native start0方法,而这些native方法的注册是在Thread对象初始化的时候完成的

/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
    registerNatives();
}

在thread 中的registerNatives静态方法中 有很多初始化方法,thread被加载时候就会调用这个本地方法 thread.c 注册start0``stop0等等 :

/* Some private helper methods */
private native void setPriority0(int newPriority);
private native void stop0(Object o);
private native void suspend0();
private native void resume0();
private native void interrupt0();
private native void setNativeName(String name);

不论是实现runnable 接口,还是继承thread类,最后其实都是按照这个流程实现新线程的建立的。 如果不是通过start()而是调用run()方法的话在代码的运行顺序是:


/* What will be run. */

class Thread implements Runnable{
private Runnable target;

public void run() {
    if (target != null) {
        target.run();
    }
}
}


public interface Runnable {
 
    public abstract void run();
    }

在这里他就直接去执行runnable接口的run了,而缺失了上面JNI方法的运行了,从而不能真正的实现新线程的创建。

object : notify/wait

object.wait:当前线程等待 object.notify :通知等待的线程开始运行 多个线程等待时会随机唤醒一个线程 notifyAll:唤醒所有的线程

线程协作

join() 会等待依赖的线程结束,内部调用了wait():

public final synchronized void join(long millis)
throws InterruptedException {
 long base = System.currentTimeMillis();
 long now = 0;

 if (millis < 0) {
     throw new IllegalArgumentException("timeout value is negative");
 }

 if (millis == 0) {
     while (isAlive()) {
         wait(0);
     }
 } else {
     while (isAlive()) {
         long delay = millis - now;
         if (delay <= 0) {
             break;
         }
         wait(delay);
         now = System.currentTimeMillis() - base;
     }
 }
}

1.2 JMM:Java内存模型

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。 在并发编程领域,有两个关键问题:线程之间的通信和同步。

线程之间的通信

线程的通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种共享内存和消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。

在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。

关于Java线程之间的通信,可以参考线程之间的通信(thread signal)。

线程之间的同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。

在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。

在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发采用的是共享内存模型

Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

synchronized 关键字:

  1. 实现线程间同步,保证线程安全性
  2. 对象加锁,只有一个线程能进入同步块
  3. 可以作用于:
  • 代码块:获得给定对象object的锁才能进入同步代码块,对给定的对象加锁
  • 实例方法:获得当前实例instance的锁
  • 静态方法:获得当前类class的锁

jdk 1.5以后进行了性能的优化:

由于在大多数情况下,锁都会被一个线程去获取,这时没必须要去阻塞其他线程,这样会导致频繁的上下文切换,进而性能会降低很多,所以JDK1.5之后引入CAS来优化Synchronized。CAS采用循环等待的方法,当线程没有获取到锁时,会进一步循环尝试,不会果断的挂起阻塞,当CAS操作失败后由程序员自己判断是否将其挂起还是执行其他线程。

  • 在使用Synchronized锁时,相当于是一种悲观锁,在获取到锁的线程角度理解就是在任何时刻都会有别的线程会和我去竞争锁,所以当一个线程获取到锁时就会阻塞其余线程,锁的粒过大。而CAS则是一种乐观锁策略(无锁),表示在任何情况下都不会有其他线程与我去竞争锁资源,既然没有冲突自然不需要阻塞其他线程

  • CAS思想:在CAS中有三个标记 :内存中的实际值(V) 预期的值(O 旧值) 更新的值(N)

  • 当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。

CAS的缺点:

  • 若循环时间过长,则开销很大 解决方法: 自适应自旋
  • 只能保证一个共享变量的原子操作 解决方法:当要操作多个共享变量时,需要使用锁
  • 公平性:自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。因此Synchronized是非公平锁

lock 锁

控制线程是否访问临界区的资源

  • 可重入锁 reentranLock 一个线程可以连续两次获得同一把锁 同一线程多次获得锁,必须释放和获得同样的次数
  • 尝试获得锁,减少死锁的可能性: try lock(3,miles) 两个单位:等待的时间长度,时间的单位
  • 公平锁 每个线程获得资源的公平性不同,默认随机,非公平 公平锁可以保证线程按照时间先后顺序先进先出来得到资源
  • 读写分离锁 readlock writelock 读不通过修改数据实现优化,只有写和写之间,读和写之间才加锁 大部分系统读的次数远远大于写的次数

1.3 线程集合

java提供了一个原子操作的包用来支持atomic原子操作 java.util.concurrent.atomic

cas

compare And Swap 比较和交换 cpu指令支持原子化的cas cas有三个操作数: 内存值 v,旧的预期值a,要修改的新值B 当且仅当A=V时候,将V修改为B,否则不做操作 compareAndSet利用JNI来完成cpu指令的操作

public final boolean compareAndSet(long expect, long update) {
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

通过cmpxchgl这个指令在cpu级来做这个操作

AtomicInteger

  • 线程安全的可变integer
  • 当前实际取值value
  • 偏移量valueOffset
  • 加一并返回值,无需加锁
  • 内部实现:cas控制不断尝试直到成功:使用jni的本地方法实现
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  • 类似的有:AtomicBoolean AtomicLong

AtomicReference

封装的是对象,保证修改对象引用时的线程安全性 判断正确写入的条件:当前的值和期望的值是否一样 如果原值被修改则无法判定,需引入时间戳 当 AtomicStampedReference设置对象值时候,对象值和时间戳都必修满足期望的值

线程安全的集合类型:

ConcurrentHashMap
ConcurrentLinkedDeque
CopyOnWriteArrayList

threadLocal

线程局部变量 getMap()拿到Thread内部的ThreadLocalMap 线程退出之前要置空或者调用remove方法 在spring里面广泛得到了运用,是线程安全的关键

Future

异步调用, 服务程序可以不需等待完成,可以先处理其他业务逻辑,实现解耦

实现和继承 runnableFuture/FutureTask

  • 实现callable接口,有call方法

  • 用futureTask进行封装

  • 将FutureTask提交给线程池执行

1.3 问题

Volatile为啥不能保证复合操作(如:i++)的原子性

Volatile在java中建议使用atomic operation ,也就是用原子类型的操作。i++是不是原子类型的操作呢?i++做了三次指令操作,两次内存访问,第一次,从内存中读取i变量的值到CPU的寄存器,第二次在寄存器中的i自增1,第三次将寄存器中的值写入内存。这三次指令操作中任意两次如果同时执行的话,都会造成结果的差异性。而对于++i,在多核机器上,CPU在读取内存时也可能同时读到同一个值,这样就会同一个值自增两次,而实际上只自增了一次,所以++i也不是原子操作。 Volatile的作用是:1.保障java内存可见性2.保证有序性,通过禁止指令重排序和实现 happen before原则 。 Volatile并不保证原子性 , 如果要实现原子性可以使用java 的java.util.concurrent.atomic包中的AtomicLongFieldUpdater 去操作Volatile类型,从而实现i++同等意义的操作,但是保证执行的正确

ThreadLocal中最后为什么要加remove方法?

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本。ThreadLocal 的内部结构中每一个 Thread 都有一个 ThreadLocal。ThreadLocalMap 这样的类型变量,变量的名字叫作 threadLocals。

线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。

GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,jdk的开发者考虑到了这个问题 所以早在entry的实现上就使用了WeakReference弱引用。这样就避免了内存泄露的问题

static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
}

但是我们继续看这部分源码, 可以看到,虽然整个Entry避免了强引用导致的内存泄露,但是value = v 这行代码还是发生了强引用,正常情况下,当线程终止,key 所对应的 value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能 ThreadLocal 以及它所对应的 value 早就不再有用了。会导致:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例。 如何来避免这个问题呢 解决的方法就是remove方法:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

可以看出,它是先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法。这里的 remove 方法可以把 key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。

所以,在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生。