从Java并发编程到升职加薪之线程入门

1. 基本概念

1.1 什么是进程和线程

进程:操作系统分配资源的最小单元,这里的资源指的是CPU,内存和磁盘IO等。进程与进程之间是相互独立的。

线程:CPU调度的最小单位,线程不能独立存在,必须依赖与进程而存在。一个进程内可以有多个线程。线程可以共享进程内的资源。

在linux环境下,一个进程所能开的最大线程数为1000个,windows环境下,一个进程所能开的最大线程数为2000个。

1.2 CPU核心和线程数的关系

正常情况下CPU核心数与线程数是1:1关系,但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。一个CPU核心执行一个线程。

1.3 CPU时间片轮转机制

CPU时间片轮转机制(也称为RR调度)是最古老,最公平,最简单的算法。通过将CPU时间进行切片,每个进程分配一个时间段,进程在对应的时间段中执行任务。CPU执行一条指令的时间近乎是纳秒(ns)级别。由于是轮转执行任务,所以当A线程执行完毕后,需要将B线程执行时所要耗费的资源加载到CPU中,这种现象称为上下文切换。上下文切换需要耗费较多的CPU资源,大约20000个CPU周期。

1.4 并行与并发

并行:多个线程同时执行,每个线程使用线程内部的资源,各走各的,互不干扰。

并发:多个线程交替对共享资源进行操作,讨论并发的时候一定要加个单位时间,就是说单位时间内并发是多少?时间片轮转机制也是一种并发算法~

2. Java的线程

Java 里的程序天生就是多线程,有两种新启线程的方式:继承Thread;实现Runnable,实际上通过实现 Callable 也可以新启线程。但是根据 java.lang.Thread 源码的描述,新启动线程有两种方式。Jdk 的线程是协作式而不是抢占式

img

为什么说Java程序天生多线程?

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class TestApp {

    public static void main(String[] args) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println(threadInfo.getThreadId() + ": " + threadInfo.getThreadName());
        }
    }
}

控制台输出以下信息

# 监听Ctrl-break终端信号
6: Monitor Ctrl-Break 
# 内存dump,线程dump,类信息统计,获取系统属性等
5: Attach Listener
# 分发处理发送给jvm信号的线程
4: Signal Dispatcher            
# 执行 Object.finalize() 的方法,用来回收资源,一般很少重写Object.finalize()方法
3: Finalizer
# 清除Reference的线程
2: Reference Handler
# main线程,用户程序入口
1: main

2.1 Thread与Runnable的区别

Thread是Java对线程的唯一抽象,Runnable是对任务的抽象,Thread可以接受任意一个Runnable的实例并执行。

2.2 线程的生命周期

img

2.3 线程中止

线程的中止包括自然终止和程序干预两种

  • 线程自然终止:要么是run()执行完成;要么是抛出一个未处理的异常导致线程提前结束。

  • 程序干预:

    • 暂停(suspend)恢复(resume)停止(stop)都是Thread类操作线程的API,但是这些API是过期的,也就是不建议使用的方法。之所以不建议使用,是因为这些方法比较野蛮,以stop() 为例,执行stop就强制中断线程的执行,可能会导致线程资源释放不及时。
    • 安全的中止是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,执行该方法只是将线程A标记为中断状态,使用isInterrupted() 判断线程是否被中断,从而手动的关闭线程。

不建议使用自定义的标志位来中止线程的运行。因为run方法里有阻塞调用时无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。而对于中断而言,一般阻塞方法如sleep等本身就支持中断检查。注意处于死锁的线程无法被中断!!

public class TestThread {

    private static class UserThread1 extends Thread {

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread() + " is running...");
            }
        }
    }

    private static class UserThread2 extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread() + " interrupt before: " + this.isInterrupted());
            // 判断当前线程是否为中断状态,将状态标记为true
            // 也可以使用 Thread.interrupted() 方法判断中断状态, 将状态标记为false
            while (!this.isInterrupted()) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    // 发生中断异常,但是中断标记还是为false,允许手动处理资源的释放,然后再调用this.interrupt();去关闭这个线程
                    System.out.println(Thread.currentThread() + " interrupt after: " + this.isInterrupted());
                    // 释放资源的动作
                    // ...
                    // 执行该方法后将中断标记修改为true,表示线程结束
                    this.interrupt();
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + " is running...");
            }
            System.out.println(Thread.currentThread() + " interrupt after: " + this.isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new UserThread1();
        thread1.start();
        Thread.sleep(2000);
        // 已废弃,jdk不建议使用
        // stop 会强制将线程中断, 导致线程所占用的资源不能正常释放
        // thread.stop();
        // 标记线程为中断状态,不会立刻中断线程的活动
        // 线程也可以完全不理会该状态继续执行, 如该程序中thread1会一直输出 running...
        thread1.interrupt();
        Thread thread2 = new UserThread2();
        thread2.start();
        Thread.sleep(2000);
        thread2.interrupt();
    }
}

2.4 start()与run()的区别

start() 用来启动一个操作系统的线程,只允许执行一次,否则会抛出 IllegalThreadStateException 异常

img

run() 方法可以看成一个类的普通方法,允许多次执行,且执行run()方法不会新启线程。

2.5 wait()和notify()/notifyAll()

注意wait()与notify()/notifyAll()方法是Object类中定义的方法

  • wait():调用该方法的线程进入waiting状态,只有等待另外线程的通知或被中断才会返回,需要注意的是,调用wait()方法后,会释放对象的锁。
  • wait(long):超时等待一段时间,参数是毫秒,这边注意的是,在未超时之前是由notify或notifyAll方法通知返回,但是只要超时就一定会返回。
  • notify():通知任意一个在对象上等待的线程,让这个线程先去争抢该对象的锁,如果抢到则从wait方法返回,如果没有抢到锁则重新进入waiting状态。执行该方法不会释放对象的锁
  • notifyAll():通知所有等待在该对象上的线程,执行该方法不会释放对象的锁
2.5.1 等待/通知机制

wait()notify/notifyAll()构成了一个简单的等待/通知机制:线程A调用对象O的wait()方法进入等待状态,线程B调用对象O的notify或者notifyAll()方法,线程A收到通知后从对象O的wait() 方法返回,进而执行后续操作。

2.5.2 等待和通知的标准范式

等待方遵循如下原则:

  • 获取对象的锁
  • 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
  • 条件满足则执行对象的逻辑
synchronized (对象) {
    while (条件不满足) {
        // 一直等待
        对象.wait();
        // 超时等待,1s后直接返回
        对象.wait(1000)
    }
    // 对应的处理逻辑
}

通知方遵循如下原则:

  • 获取对象的锁
  • 改变条件
  • 通知所有等待在对象上的线程
synchronized (对象) {
    // 改变条件
    // 因为notify/notityAll方法不会释放对象的锁,所以一般放在同步块或者同步方法最后一行
    // 通知在对象上等待的任意一个线程
    对象.notify();
    // 通知在对象上等待的所有线程
    对象.notifyAll();
}

由标准范式中可以看出,调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,就是说只能在同步方法或同步块中调用wait()、notify() 系列方法。

2.5.3 实现一个简单的连接池

使用等待通知机制实现一个简单的数据库连接池,归还连接的时候,将连接尾插入链表中,然后通知在POOL对象上等待的所有线程起来干活。获取连接时,先判断POOL是否为空,如果为空则将线程wait(),否则返回连接对象。

超时模式时需要多判断超时时间,这边使用 POOL.wait(t) 实现等待超时,如果超时则直接返回,并且修改等待时间,如果超时,则判断下此时的连接池是否为空,不为空返回连接对象,为空则直接返回null

public class DBPool {

    private static final ConcurrentLinkedQueue<Connection> POOL = new ConcurrentLinkedQueue<>();

    public DBPool(int initialSize) {
        /* 初始化连接数 */
        for (int i = 0; i < initialSize; i++) {
            POOL.add(new SqlConnectionImpl());
        }
    }

    public void releaseConnection(Connection con) {
        if (con != null) {
            synchronized (POOL) {
                POOL.add(con);
                // 通知其他线程获取连接
                POOL.notifyAll();
            }
        }
    }

    public Connection getConnection(long timeout) throws InterruptedException {
        if (timeout <= 0) {
            // 无超时模式
            synchronized (POOL) {
                while (POOL.isEmpty()) {
                    // 线程挂起,等待通知
                    POOL.wait();
                }
                return POOL.remove();
            }
        } else {
            long timeoutTime = System.currentTimeMillis() + timeout;
            long t = timeout;
            synchronized (POOL) {
                while (POOL.isEmpty() && t > 0) {
                    POOL.wait(t);
                    // 重置超时时间
                    t = timeoutTime - System.currentTimeMillis();
                }
            }
            if (!POOL.isEmpty()) {
                return POOL.remove();
            }
            return null;
        }
    }
}

测试程序:这边使用 CountDownLatch 进行多线程统计

import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class DBPoolTest {
    private static final DBPool pool  = new DBPool(10);
    // 控制器:控制main线程将会等待所有Woker结束后才能继续执行
    static CountDownLatch end;

    public static void main(String[] args) throws Exception {
        // 线程数量
        int threadCount = 50;
        end = new CountDownLatch(threadCount);
        //每个线程的操作次数
        int count = 20;
        //计数器:统计可以拿到连接的线程
        AtomicInteger got = new AtomicInteger();
        //计数器:统计没有拿到连接的线程
        AtomicInteger notGot = new AtomicInteger();
        AtomicInteger errGot = new AtomicInteger();
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new Worker(count, got, notGot, errGot),
                    "worker_"+i);
            thread.start();
        }
        end.await();// main线程在此处等待
        System.out.println("总共尝试了: " + (threadCount * count));
        System.out.println("拿到连接的次数:  " + got);
        System.out.println("没能连接的次数: " + notGot);
        System.out.println("异常报错的次数: " + errGot);
    }

    static class Worker implements Runnable {
        int           count;
        AtomicInteger got;
        AtomicInteger notGot;
        AtomicInteger errGot;

        public Worker(int count, AtomicInteger got, AtomicInteger notGot, AtomicInteger errGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
            this.errGot = errGot;
        }

        @Override
        public void run() {
            while (count > 0) {
                try {
                    // 从线程池中获取连接,如果1000ms内无法获取到,将会返回null
                    // 分别统计连接获取的数量got和未获取到的数量notGot
                    Connection connection = pool.getConnection(1000);
                    if (connection != null) {
                        try {
                            // connection.createStatement();
                            // connection.commit();
                        } finally {
                            pool.releaseConnection(connection);
                            got.incrementAndGet();
                        }
                    } else {
                        notGot.incrementAndGet();
                        System.out.println(Thread.currentThread().getName() +"等待超时!");
                    }
                } catch (Exception ex) {
                    errGot.incrementAndGet();
                } finally {
                    count--;
                }
            }
            end.countDown();
        }
    }
}

结果如下:从结果中可以看到 拿到连接次数+没能连接的次数+异常报错的次数=总尝试拿到的连接数

image.png

2.6 yield()与join()

yield()方法:使当前线程让出CPU使用权,但让出的时间是不可设定的。也不会释放锁。说白了就是小明(线程A)说我不当班长(让出CPU使用权)了,你们爱谁干谁干,但是最终谁当班长,还是由老师(操作系统)说了算的,有可能老师下一秒又让小明当班长了,也可能换人(给其他线程分配CPU使用权)。

join() 方法:把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B中调用线程A的join方法,直到线程A执行完毕后,才会继续执行线程B。说白了就是你(线程B)求小明(线程A)办事,那么你必须等小明把事干完(等待线程A执行完毕),你才能继续干下去(线程B继续执行)。

public class UseJoin {
    
    static class Goddess implements Runnable {
        private Thread thread;

        public Goddess(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            System.out.println("Goddess开始排队打饭.....");
            try {
                if(thread!=null) {
                    thread.join();
                }
            } catch (InterruptedException e) {
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + " Goddess打饭完成.");
        }
    }

    static class GoddessBoyfriend implements Runnable {

        @Override
        public void run() {
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("GoddessBoyfriend开始排队打饭.....");
            System.out.println(Thread.currentThread().getName()
                    + " GoddessBoyfriend打饭完成.");
        }
    }

    public static void main(String[] args) throws Exception {
        GoddessBoyfriend goddessBoyfriend = new GoddessBoyfriend();
        Thread gbf = new Thread(goddessBoyfriend);
        Goddess goddess = new Goddess(gbf);
        Thread g = new Thread(goddess);
        g.start();
        gbf.start();
        System.out.println("lison开始排队打饭.....");
        g.join();
        System.out.println(Thread.currentThread().getName() + " lison打饭完成.");
    }
}

上述代码执行结果如下:main方法调用Goddess线程的join方法,那么main只有等goddess线程执行完后才能继续执行。同样的goddess线程调用GoddessBoyfriend线程的join方法,那么goddess线程必须等到GoddessBoyfriend线程执行后才能继续走下去。

image.png

2.7 用户线程与守护线程

用户线程:另起山头,即使主线程结束,也不影响用户线程的执行。

守护线程:主线程结束后,守护线程也跟着结束。可以使用 setDaemon(true) 方法将线程设为守护线程

2.8 线程间共享

线程间的共享指的是不同的线程对同一资源的操作,下面有这么一个小程序,输出的count的值在0-20000之间,导致这一现象的原因是存在不同的线程同时对count进行操作,导致最终计数不准确,这就是所谓的线程不安全问题。解决这个问题最简单的就是方法加锁,即使用 synchronized 关键字。

synchronized 确保多个线程在同一时刻,只能有一个线程处于方法或同不块中,保证了线程对变量访问的可见性和排他性,又称为内置锁机制。说白了就是一个厕所(方法或者同步块)在某一时刻只能有一个人(线程)上厕所,小明(线程A)在上厕所的时候会把门关上(加锁),小黄(线程B)只能等小明上完厕所后开门(释放锁)才能去上厕所。

synchronized 内置锁包括同步方法,同步代码块,对象锁以及类锁(锁的是类的Class对象)本质上来说都是针对某一具体对象加锁,从jvm底层来说,所谓的加锁就是针对对象的头部标记位进行数值的修改。

public class TestSync {

    private static int count = 0;

    private static class Test {
        public void add() {
            for (int i = 0; i < 10000; i++) {
                count ++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();

        new Thread(test::add).start();
        new Thread(test::add).start();

        Thread.sleep(1);

        System.out.println(count);
    }
}

2.9 错误的加锁和原因分析

2.10 Volatile 关键字

volatile 关键字是Java中最轻量的同步机制,volatile 保证了不同线程对某一变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

以下代码中,ready变量不加volatile时,子线程无法感知主线程修改了ready的值,从而不会退出循环输出number,而加了volatile关键字后,使子线程可以感知主线程修改了ready的值,从而退出循环输出number=51。但是volatile不能保证数据在多个线程下同时写的线程安全。当然volatile还能防止指令重排序,这个后面写到单例模式-双检锁的时候再深入分析。

public class VolatileCase {
    private static volatile boolean ready;
    private static int number;

    private static class PrintThread extends Thread{
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while(!ready);//无限循环
            System.out.println("number = "+number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new PrintThread().start();
        Thread.sleep(1000);
        number = 51;
        ready = true;
        Thread.sleep(5000);
        System.out.println("main is ended!");
    }
}

3. ThreadLocal 入门

Threadlocal 与Synchonized 都用于解决多线程并发访问。但是ThreadLocal与Synchonized有本质的区别。

Synchonized的原理是锁机制,使变量或代码块在某一时刻只能被一个线程访问。而ThreadLocal是为每个线程提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,从而隔离了多个线程对数据的数据共享。一个是从时间上确保了并发,一个是从空间上确保数据的唯一性。

在讲ThreadLocal之前先讲一下 Thread.threadLocals ,看源码可以知道这个是线程内部的一个变量,不同的线程都拥有各自的ThreadLocalMap。

image.png

3.1 四大引用

3.1.1 强引用

小明(变量)拿着一根钢绳(强引用)牵着一条黑狗(对象实例),在这根钢绳没断之前,其他人(GC)是没办法牵走(垃圾回收)这条狗。Object obj = new Object() 就是这类的引用。

3.1.2 软引用

小明拿着一根麻绳(软引用)牵走一条黄狗(软引用对象),在狗肉市场中狗肉量不足(发生内存溢出异常之前)的时候,屠夫(GC)会把这些黄狗列入待宰(回收)范围,第二次就直接把狗那啥了。通过继承 SoftReference 类来实现软引用。

3.1.3 弱引用

小明拿着一根细绳(弱引用)牵着一条红狗(弱引用对象),屠夫(GC)在屠宰(垃圾回收)时直接把红狗咔嚓(回收掉)了。通过继承 WeakReference 类来实现弱引用。

3.1.4 虚引用

小明拿着泡沫绳(虚引用)去牵着一条哈士奇,泡沫绳(不知道有没有这种绳子)一挣就断的那种,没办法根据绳子去找到哈士奇。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。通过继承 PhantomReference 类来实现虚引用。

3.2 ThreadLocal的使用

image.png

  • public void set(Object value):设置当前线程的线程局部变量的值

通过 set() 源码中可以看出,首先获取的是当前线程,然后从当前线程中取出 ThreadLocalMap, 如果为空则new一个,最终以ThreadLocal的弱引用为key,将value 保存在当前线程的 ThreadLocalMap中

image.png

image.png

  • public Object get():返回当前线程所拥有的线程局部变量

同样的get() 方法也是先获取当前线程的ThreadLocalMap,然后以ThreadLocal的弱引用为key获取value

image.png

  • public void remove():将当前线程局部变量的值删除。变量在使用完后要调用该方法,否则可能会造成内存泄漏。
  • protected Object initialValue():返回该线程局部变量的初始值

3.3 ThreadLocal内存泄露问题

image.png

图中虚线表示弱引用,实线表示强引用。

ThreadLocal.set() 的原理是以ThreadLocal的弱引用对象为key, 将value存放在每个线程的ThreadLocalMap中,从源码上来看,实际上是存放在ThreadLocalMap中的Entity对象中,而 ThreadLocal.Entity 是一个弱引用,通过前面介绍可以知道弱引用在GC的时候不管内存是否充足都会被回收掉。

image.png

当ThreadLocal变量置为null后,就没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被GC回收。这样一来,ThreadLocalMap中将会出现key=null的Entity,就没办法访问这些key=null的Entity的value。如果线程迟迟不结束的话,那么就会存在 Thread Ref -> Thread -> ThreadLocalMap -> Entity -> value这样的强引用链。而这块value永远不会被访问到,所以存在内存泄露。所以每次使用完后都应该调用remove()方法手动回收掉value。

3.4 ThreadLocal 可能线程不安全

用下面这段代码举例说明如果ThreadLocal使用不当,也会造成线程不安全。

public class TestThreadLocalUnsafe implements Runnable {

    public static Number number = new Number(0);

    @Override
    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum()+1);
        //将其存储到ThreadLocal中
        value.set(number);
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new TestThreadLocalUnsafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }
}

如果ThreadLocal线程安全,则每个线程输出的都应该是1,而实际上变量number被多个线程共享了。这是因为ThreadLocalMap中保存的其实是对象的一个引用,当线程A对这个引用指向的对象实例做修改时,也影响了线程B中持有的对象引用所指向的同一个对象实例。所以当线程睡眠时,其他线程将number变量进行了修改,而修改的对象number的实例是同一份,因此所有的线程最终输出的结果是相同的。

image.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值