重拾Java基础知识:多线程

前言

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。

进程和线程

进程是程序在计算机上的一次执行活动,即正在运行中的应用程序,通常称为进程,每个进程都有自己独立的内存空间(如图所示:每一个应用都是一个进程)。同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程,也称多任务。比如:你再使用电脑的同时可以上网、也可以听音乐。

注意:多进程之间相互协作,涉及到了进程通信问题。
在这里插入图片描述
线程是一个轻量级的子进程,是程序执行过程中的最小单元;进程就是由一个或多个线程构成的,每个线程是独立的,如果在一个线程中发生异常,则不会影响其他线程,它使用共享内存区域。线程由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。

注意:使用太多线程,是很耗系统资源,因为线程需要开辟内存。

并行和并发

Erlang 之父 Joe Armstron解释了并行和并发的区别,如下图所示:

  • 并发:同一时间段两个或(多个)队列,交替使用一台咖啡机(CPU/资源)。
  • 并行:同一时刻两个或(多个)队列,同时使用两个或(多个)咖啡机(CPU/资源)。
  • 串行:一个(单线程)队列,交替(前一个程序执行完下一个程序再执行)使用一台咖啡机(CPU/资源)。
    在这里插入图片描述

单核处理器下称为并发,多核处理器下可以称为并发,也可以称为并行,并行是特殊的并发(视不同情况而定)。

同步和异步

同步和异步关注的是消息通信机制,同步就是主动询问结果,异步就是被告知结果

  • 同步:当某一个线程执行一项任务时,在没有得到结果之前,线程一直等待,直到返回结果。
  • 异步:当某一个线程执行一项任务时,不需要立马返回结果,线程可以处理其它任务,通过其它回调、通知等方式返回结果。

阻塞和非阻塞关注的是程序在等待调用结果这段时间内的状态

  • 阻塞:等待调用结果期间,不能做其它任何操作。
  • 非阻塞:等待调用结果期间,可以做其它操作。

它们一共有四种情况,假设你要去书店买《Java入门到放弃》为例:

  • 同步阻塞:你去书店买《Java入门到放弃》,老板告诉你下午有货 (同步) ,你一直等着期间啥也不干 (阻塞),直到老板拿到货后你购买成功。
  • 同步非阻塞:你去书店买《Java入门到放弃》,老板告诉你下午有货 (同步) ,你每隔一段时间来询问老板有货没 (非阻塞),直到老板有货后你购买成功。
  • 异步阻塞:你去书店买《Java入门到放弃》,老板说不清楚有没有货,你留个电话,到时候打电话告诉你,等电话期间你啥也不干 (阻塞),然后老板打电话告诉你货到了 (异步),然后你去购买。
  • 异步非阻塞:你去书店买《Java入门到放弃》,老板说不清楚有没有货,你留个电话,到时候打电话告诉你,等电话期间,吃个饭上个厕所 (非阻塞),然后老板打电话告诉你货到了 (异步),然后你去购买。

线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)就绪(Runnable)运行(Running)阻塞(Blocked)死亡(Dead) 五种状态。尤其是当线程启动以后,它不能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

在这里插入图片描述

  • 新建(New)Thread类的一个实例(new)对象时,此线程进入新建状态(未被启动)。
public class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
    }
}
  • 就绪(Runnable):调用的线程的start()方法后,这时候线程处于排队等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行。
public class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

Runnable(就绪) 状态的线程无法直接进入Blocked(阻塞) 状态和Terminated(结束) 状态的。只有获得CPU调度执行权的线程才有资格进入Blocked(阻塞) 状态和Terminated(结束) 状态 ,Runnable(就绪) 状态的线程要么能被转换成Running(运行) 状态,要么被意外终止。

  • 运行(Running):线程获得CPU资源正在执行run()方法,此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

处于运行状态的线程可能会有那些改变:

  1. 被转换成Terminated(结束) 状态,比如调用 stop() 方法;
  2. 被转换成Blocked(阻塞) 状态,如进行 IO 阻塞操作,如查询数据库进入阻塞状态;
  3. 被转换成Blocked(阻塞) 状态,比如获取某个锁的释放,而被加入该锁的阻塞队列中;
  4. 该线程的时间片用完,CPU 再次调度,进入Runnable(就绪) 状态;
  5. 线程主动调用 yield() 方法,让出 CPU 资源,进入Runnable(就绪) 状态
  • 阻塞(Blocked):由于运行中的某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
  • 死亡(Dead):一旦线程进入了Terminated(结束) 状态,就意味着这个线程生命的终结,这时线程不可能再进入就绪状态等待执行。

Java线程除了以上五种状态,还存在等待(WAITING)超时等待(TIMED_WAITING) 两种状态:

  • 等待(WAITING):一个线程进入了锁,可是须要等待其余线程执行某些操做。时间不确定时,比如调用wait()join()park()方法调用时,进入等待状态。前提是这个线程已经拥有锁了。
  • 超时等待(TIMED_WAITING) :一个线程进入了锁,确认等待时间下,比如调用:sleep(long millis)wait(long timeout)join(long millis)方法,有期限等待后自动唤醒

创建线程

Java可以用四种方式来创建线程:

  • 继承Thread

重写该类的run()方法,该run()方法的方法体就代表了线程要完成的任务。

public class ThreadTest extends Thread {
    @Override
    public void run() {
        System.out.println("hello:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.run();
        /** Output:
         *  hello:main
         */
    }
}

通过查看Thread的源码,你会发现也实现了 Runnable 接口。
在这里插入图片描述

  • 实现Runable接口
public class ThreadTest implements Runnable {
    @Override
    public void run() {
        System.out.println("hello:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.run();
        /** Output:
         *  hello:main
         */
    }
}
  • 使用CallableFuture创建线程

重写call()方法,call()方法可以有返回值

public class ThreadTest implements Callable<String> {
    private String param;

    public String getParam() {
        return param;
    }

    public void setParam(String param) {
        this.param = param;
    }

    @Override
    public String call() throws Exception {
        return param;
    }

    public static void main(String[] args) throws Exception {
        ThreadTest threadTest = new ThreadTest();
        threadTest.setParam("param");
        String call = threadTest.call();
        System.out.println(call);
        /** Output:
         *  param
         */
    }
}

Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

public class ThreadTest implements Callable {
    @Override
    public Object call() throws Exception {
        return Thread.currentThread().getName();
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        FutureTask<String> futureTask = new FutureTask<>(threadTest);
        new Thread(futureTask).start();
        try {
            String s = futureTask.get();
            System.out.println(s);
            /** Output:
             *  Thread-0
             */
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Future 类,有几个比较重要的方法,简单讲解下:

public class ThreadTest implements Future {
    //试图取消此任务的执行
    @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
        return false;
    }
    //如果任务在完成之前被取消,返回true否则false
    @Override
    public boolean isCancelled() {
        return false;
    }
    //任务完成(异常终止),返回true
    @Override
    public boolean isDone() {
        return false;
    }
    //获取返回值,必须等到子线程结束后才会得到返回值,否则会造成拥堵
    @Override
    public Object get() throws InterruptedException, ExecutionException {
        return null;
    }
    //设置最长堵塞时间,超出时间抛异常
    @Override
    public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return null;
    }
    
}
  • 匿名内部类创建

有两种方式创建内部类:

public class ThreadTest {

    public static void main(String[] args) {
        //使用Thread匿名内部类
        new Thread(){
            @Override
            public void run() {
                System.out.println("run");
            }
        };
        //使用Runnable匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("run");
            }
        });
    }

}
  • lamada表达式创建

使用lamada创建线程更加的简洁:

public class ThreadTest {

    public static void main(String[] args) {
        new Thread(()->{
            for (int i = 0; i < 12; i++) {
                System.out.println(i);
            }
        }).start();
    }

}
  • 通过线程池创建

比如使用Executor框架,后续讲解,再实际开发中,用的最多的也是通过线程池去创建线程。

获取线程对象

我们可以通过Thread.currentThread()方法获取一些线程的信息:

public class ThreadTest implements Runnable {
    @Override
    public void run() {
        System.out.println("run");
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());//获取线程名称
        System.out.println(Thread.currentThread().getState());//获取线程状态
        System.out.println(Thread.currentThread().getId());//获取该线程的标识符,生命周期内唯一
        System.out.println(Thread.currentThread().getPriority());//获取该线程的优先级
        System.out.println(Thread.currentThread().isAlive());//是否处于活动状态
        System.out.println(Thread.currentThread().isDaemon());//程是否是守护线程
        System.out.println(Thread.currentThread().isInterrupted());//是否已被中断
        /** Output:
         *  main
         *  RUNNABLE
         *  1
         *  5
         *  true
         *  false
         *  false
         */
    }
}

当然还有很多其它方法,有兴趣可以自己去了解。

线程睡眠(sleep)

线程休眠的目的是使线程让出CPU的最简单的做法之一,线程休眠时候,会将CPU资源交给其他线程,以便能轮换执行,当休眠一定时间后,线程会苏醒,进入准备状态等待执行。

public class ThreadTest extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName());
            if(i == 3){
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        ThreadTest thread = new ThreadTest();
        thread.start();
        ThreadTest thread2 = new ThreadTest();
        thread2.start();
    }

}

调用Thread.sleep(),传入指定时长(millis),达到休眠的效果。在睡眠的时候当前线程不会释放锁,当睡眠时间结束后等待执行。

礼让(yield)

调用yield()方法会让当前线程交出CPU资源,让CPU去执行其他的线程(让给谁不知道)。与sleep()区别:

  • 相同点:让出CPU;不会释放锁。
  • 不同点:yield()方法不会进入超时等待状态,重回就绪状态;yield()方法只能让拥有相同优先级的线程有获取CPU执行时间的机会(也就是说不一定保证其他线程能获取资源)。sleep()方法进入超时等待状态,结束后才有机会被重新分配CPU
public class ThreadTest extends Thread {

    @Override
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName());
        Thread.yield();
        System.out.println("礼让:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadTest thread = new ThreadTest();
        thread.start();
        ThreadTest thread2 = new ThreadTest();
        thread2.start();
    }

}

执行结果:礼让和不礼让的区别,说明礼让不是一定会执行的。

礼让:
当前线程:Thread-0
当前线程:Thread-1
礼让:Thread-0
礼让:Thread-1
不礼让:
当前线程:Thread-1
礼让:Thread-1
当前线程:Thread-0
礼让:Thread-0

合并(join)

将两个交替执行的线程合并为顺序执行的线程。join()方法为非静态方法,举个例子:比如在线程B中调用了线程Ajoin()方法,直到线程A执行完毕后,才会继续执行线程B(类似于插队)。

public class ThreadTest extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:"+Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        ThreadTest thread = new ThreadTest();
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程");
            if(i == 3){
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        /** Output:
         *  主线程
         *  主线程
         *  主线程
         *  主线程
         *  子线程:Thread-0
         *  子线程:Thread-0
         *  子线程:Thread-0
         *  子线程:Thread-0
         *  子线程:Thread-0
         *  主线程
         */
    }

}

按正常顺序是先执行主线程的任务再去完成子线程,代码中当主线程遍历第四次的时候调用了子线程的join()方法,所以就会先执行子线程任务,完成后再去执行主线程的任务。

我们可以看下join()方法的源码:

    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;
            }
        }
    }

我们发现join()方法的底层调用的是wait()方法,因为join()方法在主线程中调用,所以主线程进入等待,执行完子线程后再继续执行主线程。

注意join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,此实现使用在isAlive()条件上的wait()调用。线程终结后将会调用notifyAll()

等待(wait)和唤醒(notify)

没有哪个程序会无期限的等待下去,所以等待中的线程需要再某些时刻被唤醒​。wait()方法和notify()方法都是定义在Object类中,因为每个对象都有锁,synchronized可以操作任意对象锁。

  • wait():当前线程等待,通过调用此对象的notify()方法或notifyAll()方法唤醒。
  • wait(long timeout) :当前线程等待指定时长,通过调用此对象的notify()方法或notifyAll()方法或指定时间结束后唤醒。
  • notify():唤醒在此对象监视器上等待的单个线程。
  • notifyAll():唤醒在此对象监视器上等待的所有线程。

下面通过两个简单案例,介绍它们的使用:

public class ThreadTest extends Thread {
    public String param;

    public ThreadTest(String param) {
        this.param = param;
    }

    @Override
    public void run() {
        synchronized (param) {
            System.out.println("线程" + Thread.currentThread().getName() + "等待");
            try {
                param.wait(500);
                System.out.println("继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        String param = "thread Test";
        ThreadTest threadTest = new ThreadTest(param);
        threadTest.start();
        ThreadTest threadTest2 = new ThreadTest(param);
        threadTest2.start();
        /**  Output:
         *   线程Thread-0等待
         *   线程Thread-1等待
         *   继续执行
         *   继续执行
         */
    }
}

上面这段代码中并未通过其他线程唤醒,当时间结束后自动唤醒。

下面再来看看通过其他线程唤醒的示例:

public class ThreadTest2 extends Thread {
    public String param;

    public ThreadTest2(String param) {
        this.param = param;
    }
    @Override
    public void run() {
        synchronized (param) {
            System.out.println("唤醒所有线程");
            param.notifyAll();
        }
    }
}

执行代码:

public class ThreadTest extends Thread {
    public String param;

    public ThreadTest(String param) {
        this.param = param;
    }

    @Override
    public void run() {
        synchronized (param) {
            System.out.println("线程" + Thread.currentThread().getName() + "等待");
            try {
                param.wait(500);
                System.out.println("继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        String param = "thread Test";
        ThreadTest threadTest = new ThreadTest(param);
        threadTest.start();
        ThreadTest threadTest2 = new ThreadTest(param);
        threadTest2.start();
        ThreadTest2 threadTest3 = new ThreadTest2(param);
        threadTest3.start();
        /**  Output:
         *   线程Thread-0等待
         *   线程Thread-1等待
         *   唤醒所有线程
         *   继续执行
         *   继续执行
         */
    }
}

当多个线程访问时,调用notify()方法就会导致还有一个线程再等待,有兴趣的可以尝试执行下。

优先级

线程可以划分优先级,优先级高的线程得到的CPU资源概率较大,也就是CPU优先执行优先级高的线程对象中的任务,但是优先级低的并非没机会执行。优先级由1到10之间的数字表示,默认的优先级为5。Thread提供了三种静态变量:

  • Thread.NORM_PRIORITY(标准优先级:5);
  • Thread.MIN_PRIORITY(最小优先级:1);
  • Thread.MAX_PRIORITY(最大优先级:10);

我们可以通过getPriority()方法和setPriority(int newPriority)方法来获取线程级别和设置线程的级别,下面通过案例来介绍:

public class ThreadTest extends Thread {

    @Override
    public void run() {
        System.out.println("当前线程" + Thread.currentThread().getName() + "级别:" + getPriority());
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
        ThreadTest threadTest2 = new ThreadTest();
        threadTest2.start();
        /**  Output:
         *   当前线程Thread-0级别:5
         *   当前线程Thread-1级别:5
         */
    }
}

再来讲解设置线程级别:

public class ThreadTest extends Thread {

    @Override
    public void run() {
        System.out.println("当前线程" + Thread.currentThread().getName() + "级别:" + getPriority());
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.setPriority(10);
        threadTest.start();
        ThreadTest threadTest2 = new ThreadTest();
        threadTest2.start();
        /**  Output:
         *   第一次执行
         *   当前线程Thread-0级别:10
         *   当前线程Thread-1级别:5
         *   第二次执行
         *   当前线程Thread-1级别:5
         *   当前线程Thread-0级别:10
         */
    }
}

我们会发现,设置最高优先级,并不能保证他一定是最先执行。这是因为线程的执行有很大的随机性,它并无法控制执行哪个线程,线程优化级较高的线程不一定先执行。线程的执行顺序真正取决于CPU调度器(在Java中是JVM来调度),程序员无法控制。

守护线程

垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

public class ThreadTest2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"正在后台执行");
        }
    }
}
public class ThreadTest extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName()+"正在执行");
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
        ThreadTest2 threadTest2 = new ThreadTest2();
        threadTest2.setDaemon(true);
        threadTest2.start();
        /**  Output:
         *   Thread-0正在执行
         *   Thread-0正在执行
         *   Thread-1正在后台执行
         *   Thread-1正在后台执行
         *   Thread-1正在后台执行
         *   Thread-1正在后台执行
         *   Thread-1正在后台执行
         *   Thread-1正在后台执行
         *   Thread-1正在后台执行
         * Process finished with exit code 0
         */
    }
}

当用户线程执行完毕后,守护线程还在执行一段时间,原因是当我们销毁用户线程后也需要一段时间,因为CPU切换速度比较快,所以我们的守护线程也会运行一段时间,守护线程没有执行完毕还是退出了。

synchronized关键字

synchronized是一种同步锁,它的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。下面注意通过实现Runnable接口来介绍synchronized的使用。

修饰方法

在方法的前面加synchronized关键字,修饰方法范围是整个函数。

public class ThreadTest implements Runnable {
    @Override
    public void run() {
        method();
    }

    public void method() {
        for (int i = 0; i < 5; i++) {
            System.out.println("当前线程访问" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        thread.start();
        Thread thread1 = new Thread(threadTest);
        thread1.start();
        /**  Output:
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-1
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         */
    }
}

上面的代码没加synchronized,运行后你会看到两个线程的的执行顺序不定。下面再看看加了synchronized后的执行结果:

public class ThreadTest implements Runnable {
    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        for (int i = 0; i < 5; i++) {
            System.out.println("当前线程访问" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        thread.start();
        Thread thread1 = new Thread(threadTest);
        thread1.start();
        /**  Output:
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         */
    }
}

可以看到只有当第一个线程执行完成后才会接着执行第二个线程,这样就达到了线程同步的效果。

synchronized关键字不能继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。

修饰代码块

在之前的例子中多次使用了代码块来进行同步,这样用起来更加简洁高效。

public class ThreadTest implements Runnable {
    @Override
    public void run() {
        synchronized (this){
            for (int i = 0; i < 5; i++) {
                System.out.println("当前线程访问" + Thread.currentThread().getName());
            }
        }
    }


    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        thread.start();
        Thread thread1 = new Thread(threadTest);
        thread1.start();
        /**  Output:
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         */
    }
}

使用synchronized代码块会锁定当前(this关键字)的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象

修饰静态方法

静态方法是属于类而不属于对象的。

public class ThreadTest implements Runnable {
    @Override
    public void run() {
        ThreadTest.method();
    }

    public synchronized static void method() {
        for (int i = 0; i < 5; i++) {
            System.out.println("当前线程访问" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        thread.start();
        ThreadTest threadTest2 = new ThreadTest();
        Thread thread1 = new Thread(threadTest2);
        thread1.start();
        /**  Output:
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         */
    }
}

我们发现两个不同的线程方法不同的两个资源,它们执行时却保持了线程同步。其主要原因是,静态方法属于类,synchronized占用的锁是当前类的 class对象锁。

修饰类

和修饰静态方法一样,给这个类进行加锁:

public class ThreadTest implements Runnable {
    @Override
    public void run() {
        synchronized (ThreadTest.class){
            for (int i = 0; i < 5; i++) {
                System.out.println("当前线程访问"+Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        thread.start();
        ThreadTest threadTest2 = new ThreadTest();
        Thread thread1 = new Thread(threadTest2);
        thread1.start();
        /**  Output:
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-0
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         *   当前线程访问Thread-1
         */
    }
}

无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

Lock锁

JDK1.5后新增的ReentrantLock类同样可达到此效果,且在使用上比synchronized更加灵活。通过查看源码发现ReentrantLock类实现了Lock接口:

public class ReentrantLock implements Lock, java.io.Serializable {
}

synchronizedLock 的对比:

  • Lock是一个Java类,synchronizedJava的内置关键字。
  • Lock可以判断是否获取到锁 ,Lock需要手动释放锁,synchronized是隐式锁,出了作用域自动释放。
  • Lock只有代码块锁,synchronized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
  • Lock不会一直等待,如果获取不到锁,不用等待就可以结束,Lock适合大量同步代码的同步问题。
public class ThreadTest implements Runnable{
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        for (int i = 0; i < 5; i++) {
            System.out.println("当前线程访问"+Thread.currentThread().getName());
        }
        lock.unlock();
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        Thread thread2 = new Thread(threadTest);
        thread.start();
        thread2.start();
        /** Output:
         *  当前线程访问Thread-0
         *  当前线程访问Thread-0
         *  当前线程访问Thread-0
         *  当前线程访问Thread-0
         *  当前线程访问Thread-0
         *  当前线程访问Thread-1
         *  当前线程访问Thread-1
         *  当前线程访问Thread-1
         *  当前线程访问Thread-1
         *  当前线程访问Thread-1
         */
    }
}

上面代码中通过调用Lock.lock()方法获取锁,再执行结束后调用Lock.unlock()方法释放锁,让其他线程来占用。我们先通过一个简单的例子来介绍下Lock锁,后续再来详细介绍。

ThreadLocal

ThreadLocal线程局部变量)。每个线程都有一个变量,相同的线程中是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己线程的变量)。

示例代码:

public class ThreadTest {
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        new Thread(()->{
            threadLocal.set(1);
            System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        },"线程一").start();
        new Thread(()->{
            threadLocal.set(2);
            System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        },"线程二").start();
        System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        /** Output:
         *  线程一:1
         *  main:0
         *  线程二:2
         */
    }
}

通过重写initialValue()方法,设置初始值为0(默认为null),执行后发现每个线程赋值后在获取,并不影响其它线程。

原理分析

通过查询部分源码,我们发现底层也是类似于Map的操作,每当我们调用set()方法时,也就是在调用ThreadLocalMap,只不过key值存放ThreadLocal实例本身。

public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    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;
            }
        }//省略部分代码......
    }
}

内存泄漏

我们先来了解下强引用、软引用、弱引用和虚引用的区别:

  • 强引用(StrongReference)

最普遍的一种引用方式,如String s = “abc”,变量s就是字符串“abc”的强引用,只要强引用存在,则垃圾回收器就不会回收这个对象。

  • 软引用(SoftReference)

用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用(WeakReference)

弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。

  • 虚引用(PhantomReference)

就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

类型回收时间描述
强引用不会回收所有程序的场景,基本对象,自定义对象等
软引用内存不足时会被回收软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。一般用在对内存非常敏感的资源上,用作缓存的场景比较多,例如:网页缓存、图片缓存
弱引用更短暂的生命周期,只能存活到下一次GC前用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾。生命周期很短的对象,例如ThreadLocal中的Key。
虚引用随时会被回收, 创建了可能很快就会被回收跟踪对象被垃圾回收器回收的活动

Entry 继承WeakReferenceThreadLocalMap 中使用的 ThreadLocal作为 key ,上面介绍了弱引用的特点,也就是说下一次垃圾回收的时候必然会被清理掉。所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,但是,value 是强引用,不会被清理,这样一来就会出现 keynullvalue。如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存,无法被GC回收,导致内存泄露。

ThreadLocalMap会在set()方法,get()方法以及resize()方法等方法中对stale slots(陈旧的插槽)做自动删除(set()方法、get()方法不保证所有过期slots会在操作中会被删除,而resize()方法则会删除threadLocalMap中所有的过期slots)。

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

最好的做法是将调用threadlocalremove()方法:把当前ThreadLocal从当前线程的ThreadLocalMap中移除。(包括key,value)。

public class ThreadTest {
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

    public static void main(String[] args) {
        new Thread(()->{
            try {
                threadLocal.set(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadLocal.remove();
            }
        },"线程一").start();
    }
}

死锁

多个线程各自占有一些共享资源 , 导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形。 某一个同步块,同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题 。

线程发生死锁可能性很小,即使看似可能发生死锁的代码,在运行时发生死锁的可能性也是小之又小。

public class ThreadTest implements Runnable {

    public int p = 0;
    public Object o1 = new Object();
    public Object o2 = new Object();

    @Override
    public void run() {
        if(p == 0){
            System.out.println("当前线程:"+Thread.currentThread().getName());
            synchronized (o1){
                System.out.println("获取o1资源");
                try {
                    Thread.sleep(150);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("获取o2资源");
                }
            }
        }else{
            System.out.println("当前线程:"+Thread.currentThread().getName());
            synchronized (o2){
                System.out.println("获取o2资源");
                try {
                    Thread.sleep(150);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("获取o1资源");
                }
            }
        }

    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        Thread thread2 = new Thread(threadTest);
        threadTest.p = 0;
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadTest.p = 1;
        thread2.start();
        /** Output:
         *  当前线程:Thread-0
         *  获取o1资源
         *  当前线程:Thread-1
         *  获取o2资源
         */
    }
}

上面的一个简单案例,第一个线程先去获取o1锁后等待150毫秒,主线程等待100毫秒后,第二个线程获取o2锁等待150毫秒,此时第一个线程再去获取o2锁,此时锁在等待中,第二个线程获取o1锁时发现也没有释放锁。双方都占用锁没有释放,导致死锁。

线程安全

线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

什么是线程不安全?多个线程同时运行,可能出现多个线程先后更改数据造成所得到的数据是脏数据。

public class ThreadTest implements Runnable {

    public int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            count++;
            System.out.println(count);
        }

    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        Thread thread2 = new Thread(threadTest);
        thread.start();
        thread2.start();
        /** Output:
         *  1
         *  3
         *  4
         *  5
         *  6
         *  2
         *  7
         *  8
         *  9
         *  10
         */
    }
}

多线程编程中的三个核心概念:原子性可见性顺序性

  • 原子性:一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
  • 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 顺序性:程序执行的顺序按照代码的先后顺序执行。
public class ThreadTest implements Runnable {

    public int count = 0;

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                count++;
                System.out.println(count);
            }
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread thread = new Thread(threadTest);
        Thread thread2 = new Thread(threadTest);
        thread.start();
        thread2.start();
        /** Output:
         *  1
         *  2
         *  3
         *  4
         *  5
         *  6
         *  7
         *  8
         *  9
         *  10
         */
    }
}

本章小结

多线程是每个程序员都应该掌握的知识,对于多线程的了解越深多以后项目功能开发及框架源码的阅读,都有非常大的帮助,可以编写出非常高效的程序。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值