java多线程编程核心技术

第一章、java多线程技能

1.1进程和多线程的概念及线程的优点

线程定义:线程可以理解成是在进程中独立运行的子任务。

1.2使用多线程

使用多线程,或者继承Thread类,或者实现Runable接口
多次调用start方法会抛异常,原因是Thread类的threadStatus属性会在线程启动后改变,每次执行start方法会判断这个属性,只要值不为0就会抛IllegalThreadStateException
thread的构造函数可以传入另一个Thread,这样可以做到将Thread的run()方法交由其他的线程进行调度

代码示例:

/**
 * 如何启动一个线程
 */
public class Test01StartThread implements PrintThreadName{
    public static void main(String[] args) {
        // main函数所在线程
        Test01StartThread test01 = new Test01StartThread();
        // 继承thread
        Thread t1 = new ThreadTest();
        // 实现runnable
        Thread t2 = new Thread(new RunnableTest());

        t1.start();
        t2.start();
        test01.printThreadName();

        /*
         注意,最后一行代码是在主线程中执行的
         运行结果:
         Thread:Thread-0
         Runnable:Thread-1
         main:main
         上述运行结果输出的顺序可能变化,但每行的内容不变
         执行start方法的顺序不代表线程启动/执行的顺序
          */
    }
}

/**
 * 打印类名和线程名
 */
interface PrintThreadName {
    default void printThreadName() {
        System.out.println(String.format("%s:%s", this.getClass().getSimpleName(), Thread.currentThread().getName()));
    }
}

class ThreadTest extends Thread implements PrintThreadName {
    @Override
    public void run() {
        super.run();
        printThreadName();
    }
}

class RunnableTest implements Runnable,PrintThreadName {
    @Override
    public void run() {
        printThreadName();
    }
}
共享变量

共享数据的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多个线程可以同时处理一个人的票数。

示例代码:

/**
 * 线程间共享变量
 */
public class Test02SharedVariable {
    public static void main(String[] args) {
        Thread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        Thread t2 = new Thread(myThread);
        Thread t3 = new Thread(myThread);
        Thread t4 = new Thread(myThread);
        Thread t5 = new Thread(myThread);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        /*
        如果MyThread的run方法不加锁,则运行结果如下,thread-1和thread-2出现线程不安全的现象(出现两个count:3);
        由Thread-1计算,count:3
        由Thread-5计算,count:0
        由Thread-2计算,count:3
        由Thread-4计算,count:1
        由Thread-3计算,count:2

        加锁后,运行结果则类似下述,因为给整个方法加了锁,所以每次只会有一个线程进入run方法:
        由Thread-1计算,count:4
        由Thread-5计算,count:3
        由Thread-4计算,count:2
        由Thread-3计算,count:1
        由Thread-2计算,count:0
         */
    }
}

class MyThread extends Thread {
    private int count = 5;
    @Override
    synchronized public void run() {
        super.run();
        count--;
        System.out.println(String.format("由%s计算,count:%s", Thread.currentThread().getName(), count));
    }
}

1.3 currentThread()方法

currentThread()方法可以返回代码段正在被哪个线程调用的信息。

1.4 isAlive方法

此方法可判断当前线程是否处于活动状态

1.5 sleep()方法

sleep方法作用是在指定的毫秒数内让当前“正在执行的线程”(即Thread.currentThread())休眠(暂停执行)。

1.6 getId()方法

此方法可取得线程的唯一标识。

包含currentThread()和getId()等方法的代码示例:

/**
 * 线程名称相关,当前线程和this的区别
 */
public class Test03ThreadName {
    public static void main(String[] args) {
        TestThreadName testThreadName = new TestThreadName();
        Thread t = new Thread(testThreadName);
        t.setName("a");
        testThreadName.setName("b");
        t.start();
        /*
        Thread.currentThread是当前执行线程,而this指的是运行方法的对象
        在构造方法中,由于是主线程调用TestThreadName的构造方法,所以Thread.currentThread()的线程名为main,线程id为1
        而this代表的testThreadName对象,在创建时已经自动分配了新的name和id,分别为Thread-0和11
        对象t是程序又新创建的一个对象,虽然它将testThreadName传入,但是t本身也是一个新的Thread对象,所以也重新分配了name和id,分别为Thread-1和11
        而我们将t对象的name重新set成为"a",将testThreadName对象的name重新set成为"b"
        在t线程启动后,实际上当前线程是t,但是运行的方法是在testThreadName对象中,所以出现Thread.currentThread的name为a(即t的name),this.getName()为b(即testThreadName的name)
        运行结果:
        TestThreadName--begin
        Thread.currentThread().getName()=main
        Thread.currentThread().getId()=1
        this.getName()=Thread-0
        this.getId()=11
        TestThreadName--end
        run--begin
        Thread.currentThread().getName()=a
        Thread.currentThread().getId()=12
        this.getName()=b
        this.getId()=11
        run--end
         */
    }
}

class TestThreadName extends Thread {
    public TestThreadName() {
        System.out.println("TestThreadName--begin");
        System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName());
        System.out.println("Thread.currentThread().getId()=" + Thread.currentThread().getId());
        System.out.println("this.getName()=" + this.getName());
        System.out.println("this.getId()=" + this.getId());
        System.out.println("TestThreadName--end");
    }

    @Override
    public void run() {
        System.out.println("run--begin");
        System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName());
        System.out.println("Thread.currentThread().getId()=" + Thread.currentThread().getId());
        System.out.println("this.getName()=" + this.getName());
        System.out.println("this.getId()=" + this.getId());
        System.out.println("run--end");
    }
}

1.7 停止线程

线程停止的三种方法
a、当执行完run方法后正常退出
b、使用stop方法,不推荐,和suspend和resume一样,都是废弃过期的
c、使用interrupt方法

1.7.1 停止不了的线程

interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真正停止线程。

代码示例:

class InteruptThread01 extends Thread {
    private boolean print;
    public InteruptThread01(boolean print) {
        this.print = print;
    }
    public InteruptThread01() {
        this.print = false;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t = new InteruptThread01(true);
        t.start();
        Thread.sleep(1000);
        t.interrupt();
        /*
        这里使用interrupt方法线程不会停止
         */
    }
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 500000; i++) {
            if (print) {
                System.out.println("i=" + (i + 1));
            }
        }
    }
}
1.7.2 判断线程是否是停止状态

interrupted()这个静态方法测试当前线程是否已经是中断状态,执行后具有将状态标志清除的功能。换句话说,如果连续两次调用该方法,则第二次调用将返回false。实际上内部调用了currentThread().isInterrupted(true),其中的true即清除中断状态。
isInterrupted()测试线程Thread对象是否已经是中断状态,但不清除状态标志。

代码示例:

public class Test04Interrupt {
    @Test
    public void test01() throws InterruptedException {
        Thread t = new InteruptThread01();
        t.start();
        t.interrupt();
        System.out.println("当前线程是否标识停止:" + t.interrupted()); // 实际应该使用Thread.interrupted()
        System.out.println("当前线程是否标识停止:" + t.interrupted()); // 实际应该使用Thread.interrupted()
        System.out.println("线程对象是否标识停止:" + t.isInterrupted());
        System.out.println("线程对象是否标识停止:" + t.isInterrupted());
        /*
        t.interrupted()查看的是当前线程是否被中断,但是程序运行到这里当前线程是主线程,所以当下线程标识为false
        运行结果:
        当前线程是否标识停止:false
        当前线程是否标识停止:false
        线程对象是否标识停止:true
        线程对象是否标识停止:true
         */
    }

    @Test
    public void test02() throws InterruptedException {
        Thread.currentThread().interrupt();
        Thread t = new InteruptThread01();
        t.start();
        t.interrupt();
        System.out.println("当前线程是否标识停止:" + t.interrupted()); // 实际应该使用Thread.interrupted()
        System.out.println("当前线程是否标识停止:" + t.interrupted()); // 实际应该使用Thread.interrupted()
        System.out.println("线程对象是否标识停止:" + t.isInterrupted());
        System.out.println("线程对象是否标识停止:" + t.isInterrupted());
        /*
        由于第一行就对主线程就行了interrupt,所以t.interrupted()第一次返回true,而调用一次以后状态被擦除,所以第二次调用返回false
        运行结果:
        当前线程是否标识停止:true
        当前线程是否标识停止:false
        线程对象是否标识停止:true
        线程对象是否标识停止:true
         */
    }
}
1.7.3 能停止的线程——异常法

代码示例:

class InteruptThread02 extends Thread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new InteruptThread02();
        t.start();
        Thread.sleep(1000);
        t.interrupt();
        System.out.println("end!");
        /*
        异常退出法
        运行结果:
        i=304735
        i=304736
        i=304737
        end!
        需要退出!
        进入catch方法,退出程序!
         */
    }
    @Override
    public void run() {
        super.run();
        try {
            for (int i = 0; i < 500000; i++) {
                if (this.isInterrupted()) {
                    System.out.println("需要退出!");
                    throw new InterruptedException();
                }
                System.out.println("i=" + (i + 1));
            }
        } catch (InterruptedException e) {
            System.out.println("进入catch方法,退出程序!");
            e.printStackTrace();
        }
    }
}
1.7.4 在沉睡中停止

如果线程在sleep状态下调用该线程的interrupt()方法停止某一线程,会进入catch语句,并且清除停止状态值,使之变成false。

代码示例:

class InteruptThread03 extends Thread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new InteruptThread03();
        t.start();
        Thread.sleep(1000);
        t.interrupt();
        System.out.println("main end!");
        /*
        在线程sleep时,调用interrupt方法,程序退出
        运行结果:
        run begin
        main end!
        进入catch方法,退出程序!
         */
    }
    @Override
    public void run() {
        super.run();
        try {
            System.out.println("run begin");
            Thread.sleep(200000);
            System.out.println("run end");
        } catch (InterruptedException e) {
            System.out.println("进入catch方法,退出程序!");
            e.printStackTrace();
        }
    }
}

class InteruptThread04 extends Thread {
    public static void main(String[] args) {
        Thread t = new InteruptThread04();
        t.start();
        t.interrupt();
        System.out.println("main end!");
        /*
        去掉main方法中的sleep方法调用,即先调用interrupt方法,然后线程才进行睡眠,程序依然退出
        运行结果:
        i=9998
        i=9999
        run begin
        先执行interrupt,然后线程才sleep,同样进入catch方法,退出程序!
        java.lang.InterruptedException: sleep interrupted
         */
    }
    @Override
    public void run() {
        super.run();
        try {
            for (int i = 0; i < 10000; i++) {
                System.out.println("i=" + i);
            }
            System.out.println("run begin");
            Thread.sleep(200000);
            System.out.println("run end");
        } catch (InterruptedException e) {
            System.out.println("先执行interrupt,然后线程才sleep,同样进入catch方法,退出程序!");
            e.printStackTrace();
        }
    }
}
1.7.5 能停止的线程——暴力停止

stop()方法可以直接停止线程,线程会抛出java.lang.ThreadDeath异常,但在通常情况下,此异常不需要显示地捕捉。此方法已经作废,因为强制让线程停止可能使清理工作得不到完成,另外一个情况就是对锁定的对象进行了解锁,出现数据不一致的情况。

代码示例:

/**
 * 暴力停止线程(反面示例)
 */
public class Test06Stop {
    public static void main(String[] args) {
        StopTest t = new StopTest();
        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.stop();
        t.print();
        /*
        stop直接停止进程(本质上是抛出了ThreadDeath异常,其实是个Error)
        stop方法已经废弃,因为强制让线程停止可能使清理工作得不到完成,另外一个情况就是对锁定的对象进行了解锁,出现数据不一致的情况。
        可以观察到,在catch语句中我们重新把这个ThreadDeath抛出了,但是程序不会显示地捕捉这个异常
        另外,在调用stop后,线程并没有执行完print方法而直接释放了锁,这其实是因为抛出了异常后释放了锁,所以主线程的最后可以成功调用print方法
        运行结果:
        获取锁
        i=1
        stop执行后捕获ThreadDeath异常!
        获取锁
        i=2
        释放锁
         */
    }
}

class StopTest extends Thread {
    private int i = 0;
    @Override
    public void run() {
        try {
            while (true) {
                print();
            }
        } catch (ThreadDeath e) {
            System.out.println("stop执行后捕获ThreadDeath异常!");
            throw e;
        }
    }
    synchronized public void print() {
        System.out.println("获取锁");
        i++;
        System.out.println("i=" + i);
        try {
            Thread.sleep(1100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("释放锁");
    }
}
1.7.8 使用return停止线程

可以在判断isInterrupted为true后直接return,不过还是推荐使用“抛异常”的方式,因为这种方式还可以在catch块中将异常向上抛,使线程停止的事件得以传播。

代码示例:

class InteruptThread05 extends Thread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new InteruptThread05();
        t.start();
        Thread.sleep(100);
        t.interrupt();
        System.out.println("main end!");
        /*
        使用return退出
        运行结果:
        time is : 1570698903497
        time is : 1570698903497
        main end!
        停止!
         */
    }
    @Override
    public void run() {
        super.run();
        while (true) {
            if (this.isInterrupted()) {
                System.out.println("停止!");
                return;
            }
            System.out.println("time is : " + System.currentTimeMillis());
        }
    }
}

1.8 暂停线程

1.8.1 suspend与resume方法的使用

suspend与resume方法,是暂停线程和继续执行线程的命令,已经作废,原因是他们有独占锁以及不同步的缺点。

1.8.2 suspend与resume方法的缺点——独占

如果这两个方法使用不当,极容易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。

代码示例:

/**
 * suspend和resume方法说明
 */
public class Test05Suspend {
    @Test
    public void test01() throws InterruptedException {
        SuspendThread01 thread01 = new SuspendThread01();
        thread01.start();
        Thread.sleep(100);
        thread01.suspend();
        System.out.println("main end!");
        /*
        suspend和resume方法缺点1:独占
        由于System.out.println()方法是一个同步方法,
        如果正好执行到println方法内时调用了suspend方法,则同步锁不会释放,后续对println方法的所有调用都会阻塞
        所以程序会在打印了一些数字后卡住,不会打印main end!
         */
    }
}

class SuspendThread01 extends Thread {
    private long i = 0;
    @Override
    public void run() {
        super.run();
        while (true) {
            i++;
            System.out.println(i);
        }
    }
}
1.8.3 suspend与resume方法的缺点——不同步

如果这两个方法使用不当,也容易出现因为线程的暂停而导致数据不同步的情况。

代码示例:

/**
 * suspend和resume方法说明
 */
public class Test05Suspend {
    @Test
    public void test02() throws InterruptedException {
        MyObject01 myObject01 = new MyObject01();
        Thread t1 = new Thread(() -> myObject01.setValue("a", "aa"));
        t1.setName("a");
        t1.start();
        Thread.sleep(1000);
        t1.suspend();
//        myObject01.setValue("c", "cc");
        Thread t2 = new Thread(() -> myObject01.pringUsernamePassword());
        t2.start();

        Thread.sleep(1000);
        t1.resume();
        Thread.sleep(1000);
        myObject01.pringUsernamePassword();
        /*
        suspend和resume方法缺点2:不同步
        线程t1在修改了一半数据后挂起,此时t2打印出的实际上是不一致的数据
        而后调用t1的resume方法恢复线程后,再打印的结果是正常的结果
        此外还可以注意到,虽然挂起后没有释放该方法的锁,但是t2调用的是该对象的非同步方法,不受影响。
        如果将注释的那行打开,程序就会在执行到这里时卡住
        运行结果:
        停止a线程!
        a pw
        a aa
         */
    }
}

class MyObject01 {
    private String username = "zs";
    private String password = "pw";
    synchronized public void setValue(String u, String p) {
        this.username = u;
        if (Thread.currentThread().getName().equals("a")) {
            System.out.println("停止a线程!");
            Thread.currentThread().suspend();
        }
        this.password = p;
    }
    public void pringUsernamePassword() {
        System.out.println(String.format("%s %s", username, password));
    }
}

1.9 yield方法

yield()方法的作用是放弃当前的cpu资源,将它让给其他任务(放弃的时间并不能确定)。
代码示例:

/**
 * yield方法说明
 */
public class Test07Yield {
    public static void main(String[] args) {
        Thread t = new YieldTest();
        t.start();
        /*
        yield()方法的作用是放弃当前的cpu资源,将它让给其他任务(放弃的时间并不能确定)。
        注释掉yield方法,运行结果:
        time : 20
        打开yield方法的注释,运行结果:
        time : 17518
         */
    }
}

class YieldTest extends Thread {
    @Override
    public void run() {
        super.run();
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
//            Thread.yield();
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("time : "  + (endTime - beginTime));
    }
}

1.10 线程的优先级

线程的优先级分为1~10
MIN_PRIORITY = 1;
NORM_PRIORITY = 5;
MAX_PRIORITY = 10;

1.10.1 优先级具有继承性:

继承性,如A线程中启动了B线程,则B线程的优先级与A是一样的

1.10.2 优先级具有规则性

高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。
代码示例:

import java.util.Random;

/**
 * 线程优先级说明
 */
public class Test08Priority {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            PriorityTest01 pt01 = new PriorityTest01();
            pt01.setPriority(java.lang.Thread.NORM_PRIORITY);
            PriorityTest02 pt02 = new PriorityTest02();
            pt02.setPriority(java.lang.Thread.MAX_PRIORITY);

            pt01.start();
            pt02.start();
            /*
            可以看到,从整体上看,大部分到pt02线程是先于pt01线程执行完的
            这就是线程优先级的规则性
            运行结果:
            ---------- thread 1 use time=97
            ========== thread 2 use time=97
            ---------- thread 1 use time=98
            ========== thread 2 use time=98
            ========== thread 2 use time=104
            ========== thread 2 use time=106
            ---------- thread 1 use time=105
            ---------- thread 1 use time=107
            ---------- thread 1 use time=107
            ========== thread 2 use time=107
             */
        }
    }
}

class PriorityTest01 extends Thread {
    @Override
    public void run() {
        super.run();
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            Random random = new Random();
            random.nextInt();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("---------- thread 1 use time=" + (endTime - beginTime));
    }
}

class PriorityTest02 extends Thread {
    @Override
    public void run() {
        super.run();
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            Random random = new Random();
            random.nextInt();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("========== thread 2 use time=" + (endTime - beginTime));
    }
}
1.10.3 优先级具有随机性

随机性,也就是优先级较高的线程不一定每一次都先执行完
线程的优先级,具有继承特性、规则性、随机性。后两个的意思是说优先级高的线程大部分情况是先执行完,但也有可能后执行。
守护线程:如果进程中不存在非守护线程了,则守护线程自动销毁(如何销毁?)

1.11 守护线程

java线程中有两种线程,一种是用户线程,一种是守护线程。
当进程中不存在非守护线程时,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当程序中没有非守护线程时,垃圾回收线程也就没有存在的必要了,自动销毁。

最后

1、所有代码示例都在github中
https://github.com/llbqhh/LlbStudy/tree/master/StudyJava/src/main/java/org/llbqhh/study/java/book/java_multi_thread_programming