Java并发编程(1)-并发基础(一)

一. 进程,线程

1.1进程:

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统(OS)进行资源分配的基本单位

1.2线程:

线程是进程的一个单元.是处理器(CPU)任务调度和执行的基本单位。

java运行个main函数为例. 启动时操作系统分配启动个JVM进程. main函数所在的线程就这个进程中一个线程.也称为主线程,若main方法中无多线程的程序那么这个进程就会只存在这一个线程.

1.3进程与线程的资源:

在给进程线程分配资源前,我们先简单回归下jvm内容:

  • 堆:存放对象实例即new出来的对象,数组
  • 栈:存储局部变量表、操作数栈、动态链接、方法出口等
  • 方法区:常量,静态变量,JVM加载的类
  • 程序计数器:用于保存当前线程执行的内存地址。

由此可见进程享有堆和方法区的资源. 每个线程独立享有各自的栈和程序计数器,并且共享进程中的堆和方法区资源

二.创建个线程

Thread

public class TestThread extends Thread{
    @Override
    public void run() {
        System.out.println(TestThread.currentThread().getName() + " is running");
    }
    public static void main(String[] args) {
        TestThread test = new TestThread();
        TestThread test1 = new TestThread();
        System.out.println(TestThread.currentThread().getName() + " is running");
        test.start();
        test1.start();

    }
}
输出:
main is running
Thread-0 is running
Thread-1 is running

Runnable

public class TestRunable implements Runnable{
    @Override
    public void run() {
        System.out.println(TestThread.currentThread().getName() + " 执行了");
    }
    public static void main(String[] args) {
        TestRunable test = new TestRunable();
        new Thread(test).start();
    }
}

接口与继承的区别不再赘述, 简单看下Thread类的两个构造方法

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

最终调的init

/**
*@param g线程组

*@param target被调用run()方法的对象

*@param name新线程的名称

*@param stackSize新线程所需的堆栈大小,或者

*零表示将忽略此参数。

*@param acc要继承的AccessControlContext,或者

*AccessController.getContext()如果为null

*@param inheritThreadLocals如果{@code true},则继承的初始值

*构造线程的可继承线程局部变量
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {

当线程start时候会调用.start0 native方法. 底层会调重新的run方法

private native void start0();

再看看run方法执行.

TestThread子类实现的run. 会被直接调用

public class TestThread extends Thread{
    @Override
    public void run() {
        System.out.println(TestThread.currentThread().getName() + " is running");
    }
    public static void main(String[] args) {
        TestThread test = new TestThread();
        test.start();
    }
}

而Runnable是 Thread类 调的是Thread类的run方法

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is running");
    }
});
thread.start();

Thread类 是用私有对象target执行的run, 也就是构造函数public Thread(Runnable target) {
init(null, target, “Thread-” + nextThreadNum(), 0);
} 构建对象是初始化进去的.

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

有返回值的Callable

public class CallableThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName() + " is running";
    }

    public static void main(String[] args) throws Exception {
        CallableThread callableThread = new CallableThread();
        FutureTask<String> futureTask = new FutureTask<>(callableThread);
        Thread thread = new Thread(futureTask);
        thread.start();
        String s = futureTask.get();
    }
}

上面的FutureTask构造函数入参是Callable 下面的是另一个构造函数,入参Runnable,和最终需要返回的结果.同样是线程执行完才能get到结果

FutureTask<String> futureTask1 = new FutureTask<>(new Runnable() {
    @Override
    public void run() {

    }
}, "Hello");
Thread thread1 = new Thread(futureTask1);
thread1.start();
String s1 = futureTask1.get();
System.out.println(s1);

三、wait() notify() notifyAll()

当线程执行中调用了一个共享变量的wait()方法,该线程会被阻塞挂起. (1)直到其他线程调用了共享对象的notify()或者notifyAll()方法.(2)或者调用该线程的interrupt()方法,线程会泡InterruptedException.

wait()方法可以是任意的共享对象的方法,所以wait()是object共同的父类中定义的

注意:只有获取锁的变量才能调用wait()方法,否则会报IllegalMonitorStateException异常
notify()具体唤醒哪个线程是随机的

简单的来说就是:

  • 若synchronized锁的是方法那么就当前实例来执行wait()notify()
public synchronized void method(){
    this.wait();
    notify();
}

  • 若synchronized锁的是方法所属类,那么就用类来执行wait()notify()
public Class Test{
 public static synchronized void method(){
    Test.class.wait();
 }
}

  • 若synchronized锁的是变量那么就对应变量来执行wait()notify()
public Class Test{
public Object lock = new Object();
 public static void method(){
    synchronized (lock) {
     lock.wait();
    } 
 }
}

用wait与queue写一个简单的生产者消费者例子:

public static void main(String[] args) throws InterruptedException {
     Queue<String> queue  = new ConcurrentLinkedDeque<String>();
     Thread consumer = new Thread(()-> {
         synchronized (queue) {
             while(queue.isEmpty()){
                 try {
                     queue.wait();
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
             }
             String s = queue.element();
             System.out.println(s);
             queue.notify();
         }


     });
    Thread product = new Thread(()-> {
        synchronized (queue) {
            while (queue.size()>3){
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            queue.add("1");
            queue.notify();
        }
    });
    consumer.start();
    product.start();
}

wait(long timeout) 超过指定时间后会被恢复执行

四 Thread类的两个方法join(),sleep(),yield(),yield()

join(): 主线程中,子线程对象调用join方法.主线程会一直等待子线程执行完毕

Thread product = new Thread(()-> {});
product.start();
product.join();

sleep: 当前线程让出cpu执行权 睡眠一段时间

public static native void sleep(long millis) throws InterruptedException;

yield: 平时很少会用的, 让出当前线程的剩余时间片,不会被挂起而处于就绪状态.

public static native void yield();

interrupt:线程中断,当其他线程调用当前线程的interrupt()方法. 当前线程会被打上中断标志. 执行wait等方法时会抛出InterruptedException异常

public void interrupt() {

判断当前线程是否中断状态

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

五.死锁

线程当前占有的某个资源,又竞争另一个资源该线程就会处于竞争等待状态,并且占用的资源也不会释放.
当多个线程同时占用资源,又竞争其他资源,互相处于无休止的等待状态就是所谓的死锁

下面的例子中线程0占用lockA资源,在线程1拥有lockB资源的情况下去竞争lockB资源. 线程1占用lockB资源,在线程0拥有lockA资源的情况下去竞争lockA资源. 这就发生了死锁

public class Lock {
    static String lockA = "lockA";
    static String lockB = "lockB";
    public static void main(String[] args) {
        Thread thread0 = new Thread(() -> {
            try {
                synchronized (lockA){
                    Thread.sleep(1000);
                    synchronized (lockB) {
                    }
                }
                System.out.println("thread1结束");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        });
        Thread thread1 = new Thread(() -> {
            try {
                synchronized (lockB){
                    Thread.sleep(1000);
                    synchronized (lockA) {
                    }
                }
                System.out.println("thread1结束");

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        thread0.start();
        thread1.start();
    }
}

下面修改下线程1获取资源的顺序. 可以看出获取资源的有序性可以有效的避免死锁的发生.

        Thread thread1 = new Thread(() -> {
            try {
                synchronized (lockA){
                    Thread.sleep(1000);
                    synchronized (lockB) {
                    }
                }
                System.out.println("thread1结束");

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        thread0.start();
        thread1.start();
    }

六、用户线程与守护线程

main方法执行的时候会启动一个用户线程(user),main函数所在的线程就是用户线程. 同时jvm还会启动一些垃圾回收等守护线程(daemon).

当所有的用户线程都结束了,无论是否存在守护线程JVM都会退出.

我们正常new Thread出来的线程都是子线程,同样属于用户线程. 如果想要把thread设置为守护线程可以执行setDaemon方法.

Thread thread1 = new Thread(() -> {
            try {
               Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
thread1.setDaemon(true);

实际应用:

  • Tomcat中用于接收用户链接请求的线程都是守护线程,当tomcat shutdown后tomcat会立即停止,不会处理当前正在进行的请求

七、ThreadLocal

ThreadLocal(线程变量).每个线程获取与填充都操作当前线程对应的变量副本. 不同线程间的变量副本是隔离开的

static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
    threadLocal.set("main");
    new Thread(()->{
        threadLocal.set("thread1");
    }).start();

    new Thread(()->{
        threadLocal.set("thread2");
        System.out.println("thread2:"+threadLocal.get());
    }).start();
    System.out.println("main:"+threadLocal.get());
}


控制台:
thread2:thread2
main:main

从功能上来ThreadLocal或许是一个容器,key是线程id,value是线程的拥有的对象数据. 如果我设计的话可能ThreadLocal会设计成上述这种.

MyThreadLocal:

  public class MyThreadLocal<T>{
    Map<Thread, T> local= new HashMap<Thread, T>();
    T get(){
        Thread thread = Thread.currentThread();
        T o = local.get(thread);
        return o;
    }
    void set(T t){
        Thread thread = Thread.currentThread();
        local.put(thread, t);
    }

}
public static void main(String[] args) throws InterruptedException {
    MyThreadLocal<String> threadLocalUtil = new MyThreadLocal<>();
    threadLocalUtil.set("main");
    System.out.println(threadLocalUtil.get());

    new Thread(()->{
        threadLocalUtil.set("thread2");
        System.out.println("thread2:"+threadLocalUtil.get());
    }).start();

}

来看看这种实现的缺陷:

  • ThreadLocal就是为多线程所设计的,所以一定涉及并发问题.HashMap线程并不是安全的,所以如果这样实现也要用ConcurrentHashMap<Thread, String> map = new ConcurrentHashMap<>();

  • ThreadLocal的思想是每个线程持有自己的副本,不存在竞争关系. 上面的实现无论是用hashMap,还是ConcurrentHashMap,或者是锁 都会造成资源的竞争.

  • 如果在主线程中new MyThreadLocal();. 只要主线程一直存在,MyThreadLocal引用就一直在,即使使用过MyThreadLocal的子线程已经结束了 .map容器中子线程持有的内容也不会被GC掉. local对象会越来越大.

点进原码看看真正的ThreadLocal是怎么设计的:

  • ThreadLocalMap: ThreadLocalMap是ThreadLocal的静态内部类,继承了WeakReference的一个map结构.(WeakReference:弱引用只要发生的gc 对象就会被回收.具体可以看下https://blog.csdn.net/javaphpsqlmysql/article/details/135956350?spm=1001.2014.3001.5501)
  • threadLocals: ThreadLocal的私有对象,类型是ThreadLocalMap. 数据都存放在各自线程的私有对象的这个map里边,所有称之为数据副本,线程间互相隔离.

从set方法开始,线程首次调用ThreadLocal的set方法时threadLocals是空的,走到createMap. new了一个ThreadLocalMap.

用ThreadLocal对象set值:

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


获取当前线程的私有对象threadLocals

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

创建一个map 并将值存到这个map中

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

new Entry(firstKey, firstValue)可以看到这个map的key就是ThreadLocal对象本身. value就是需要保存的值 key是ThreadLocal对象本身是因为每个线程可以使用多个ThreadLocal去保存值(

  • ThreadLocal threadLocal0 = new ThreadLocal<>();
  • ThreadLocal threadLocal1 = new ThreadLocal<>();
  • threadLocal0.set(int)
  • threadLocal0.set(string)

)

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

set方法搞清楚了 get方法也都能想到.就是去thread的私有对象threadLocalMap中通过当前调用的threadLocal对象作为key取值.

总体了解了ThreadLocal我们再回顾看下实现细节

为什么要继承弱引用WeakReference.

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
       
        Object value;

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

那我们反过来看下强引用会发生什么,我们系统中可能会持续运行多个线程,系统中的线程又大部分来源于线程池的复用,线程的周期很长.每个线程中都运行如下代码ThreadLocalMap会存放了大量数据.

public  void systemOut() {
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    threadLocal.set("BIG_DATA");
    System.out.println(threadLocal.get());
}

方法执行结束了 jvm的栈中threadLocal对象已经没了. 但是ThreadLocalMap仍然持有key和"BIG_DATA"的强引用, 所有不会被GC掉.ThreadLocalMap数据就会越来越大

Entry继承了WeakReference就是为了解决这个问题,把key设置成弱引用.一旦threadLocal的强引用消失,下一次垃圾回收ThreadLocalMap中的key就会被gc掉.注意这里只有key是弱引用. value不是,value不会被gc.

后续get set时会触发一些清除操作,将key为null的value remove掉. 但是这个并不是固定的,在使用ThreadLocal时大家还是要在不需要的时候手动remove掉,尽可能的阻止内存溢出.

ThreadLocal.png

从图来看会清晰一点,看下弱引用的作用就是为了当threadLocal为null也就是强引用1不存在的时候.能够吧key1垃圾回收起来

来思考个小问题

value不会被gc可能会造成内存溢出, 为什么不参考key把key、value都搞成弱引用.没有强引用时都给Gc掉

public static void main(String[] args) {
    //假设key value都是弱引用
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    threadLocal.set("main");
    new Thread(()->{
        threadLocal.set("thread1");
    }).start();
    //模拟jvm发生了垃圾回收
    System.gc();
    System.out.println("main:"+threadLocal.get());
}

控制台:
main:null

上面的代码如果value也是弱引用的话,threadLocal.get()取出来的就是空的了. 因为main方法没结束ThreadLocal threadLocal对象的引用还在栈中,key不会被回收. 但是value也就是字符串"main"没强引用指向他了直接被垃圾回收了!

下面我们继续看下用于储存的数据结构ThreadLocalMap中的细节

ThreadLocalMap的内部类:Entry

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {

ThreadLocalMap的构造函数

  • new一个初始大小为Entry16数组
  • 计算key的hashCode与数组长度与运算得到下标. threadLocalHashCode里边的内容大致和.hashCode差不多,只不过threadLocalHashCode的散列性更好
  • 设置大小为初始值1
  • 设置阀值,调整大小阈值以在最坏情况下保持2/3的负载系数。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

hash算法大概是每次使用AtomicInteger原子性的的增加0x61c8864.能够使生成的哈希值足够分散

 private static final int HASH_INCREMENT = 0x61c88647;
     private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

ThreadLocal的set方法

  • Entry e = tab[i]; e != null;命中了这个条件说明尽管hash足够分散仍然发生了hash碰撞.
  • e = tab[i = nextIndex(i, len)]通过线性寻址法找到下一个空的位置.把数据存进去 e.value = value;
  • 线性寻址set和get都会用到.在寻址的过程中会识别到null key的数据,就是前边提到的弱引用.将key为空的元素删除,其余数据向前移动.
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

线性寻址:当前位置有冲突,就去按顺序依次寻找当前位置+1、+2、+3的地方是否是空的, 如果到了数组的末尾就从0开始直到找到空余位置. 线性寻址是最为简单直接的hash冲突解决方式.其他还有二次寻址、链表、双重散列等.

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

ThreadLocal没有继承关系,也就是子线程拿不到父线程的副本. InheritableThreadLocal补充了这个功能

ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("main");
new Thread(()->{
    
    System.out.println("thread2:"+inheritableThreadLocal.get());
}).start();

控制台输出 main




ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("main");
new Thread(()->{
    //覆盖父线程内容
    inheritableThreadLocal.set("thread2");
    System.out.println("thread2:"+inheritableThreadLocal.get());
}).start();


控制台输出 thread2

InheritableThreadLocal继承了ThreadLocal 重写了createMap等方法.inheritableThreadLocals取代了ThreadLocal中的对象

void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

看一眼Thread的构造函数 ,其中有段逻辑是判断当前线程inheritThreadLocals是否有值有的话调用ThreadLocal.createInheritedMap批量复制过去

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

因为是创建线程时复制进去的,在子线程new出来之后,父线程向inheritableThreadLocal录入的数据子线程是不会拿到的

ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
new Thread(()->{
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("thread2:"+inheritableThreadLocal.get());
}).start();
inheritableThreadLocal.set("main");

控制台:
thread2:null

InheritableThreadLocal的应用:

  • 子线程需要父线程的登录态信息
  • 日志链路id等
  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值