Java多线程编程核心技术(一)Java多线程技能

一、进程和线程

一个程序就是一个进程,而一个程序中的多个任务则被称为线程。html

进程是表示资源分配的基本单位,线程是进程中执行运算的最小单位,亦是调度运行的基本单位。java

举个例子:编程

打开你的计算机上的任务管理器,会显示出当前机器的全部进程,QQ,360等,当QQ运行时,就有不少子任务在同时运行。好比,当你边打字发送表情,边好友视频时这些不一样的功能均可以同时运行,其中每一项任务均可以理解成“线程”在工做。安全

二、使用多线程

在Java的JDK开发包中,已经自带了对多线程技术的支持,能够很方便地进行多线程编程。实现多线程编程的方式有两种,一种是继承 Thread 类,另外一种是实现 Runnable 接口。使用继承 Thread 类建立线程,最大的局限就是不能多继承,因此为了支持多继承,彻底能够实现 Runnable 接口的方式。须要说明的是,这两种方式在工做时的性质都是同样的,没有本质的区别。以下所示:多线程

1.继承 Thread 类并发

public class MyThread extends Thread {

    @Override
    public void run() {
        //...
    }
    
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

2.实现 Runnable 接口异步

public static void main(String[] args) throws InterruptedException {
     new Thread(new Runnable() {
         @Override
         public void run() {
             //...
         }
     }).start();
 }

Thread.java 类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,也就是使线程获得运行,多线程是异步的,线程在代码中启动的顺序不是线程被调用的顺序。ide

Thread构造方法

Thread()
分配新的 Thread 对象。
Thread(Runnable target)
分配新的 Thread 对象。
Thread(Runnable target, String name)
分配新的 Thread 对象。
Thread(String name)
分配新的 Thread 对象。
Thread(ThreadGroup group, Runnable target)
分配新的 Thread 对象。
Thread(ThreadGroup group, Runnable target, String name)
分配新的 Thread 对象,以便将 target 做为其运行对象,将指定的 name 做为其名称,并做为 group 所引用的线程组的一员。
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
分配新的 Thread 对象,以便将 target 做为其运行对象,将指定的 name 做为其名称,做为 group 所引用的线程组的一员,并具备指定的堆栈大小
Thread(ThreadGroup group, String name)
分配新的 Thread 对象。

三、实例变量与线程安全

自定义线程类中的实例变量针对其余线程能够有共享与不共享之分。当每一个线程都有各自的实例变量时,就是变量不共享。共享数据的状况就是多个线程能够访问同一个变量。来看下面的示例:学习

public class MyThread implements Runnable {
    private int count = 5;

    @Override
    public void run() {
        count--;
        System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count);
    }
}

以上代码定义了一个线程类,实现count变量减一的效果。运行类Runjava代码以下:测试

public class Ruu {

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread a = new Thread(myThread,"A");
        Thread b = new Thread(myThread,"B");
        Thread c = new Thread(myThread,"C");
        Thread d = new Thread(myThread,"D");
        Thread e = new Thread(myThread,"E");
        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

打印结果以下:

线程C 计算 count = 3
线程B 计算 count = 3
线程A 计算 count = 2
线程D 计算 count = 1
线程E 计算 count = 0

线程C,B的打印结果都是3,说明C和B同时对count进行了处理,产生了“非线程安全问题”。而咱们想要的获得的打印结果却不是重复的,而是依次递减的。

在某些JVM中,i--的操做要分红以下3步:

  1. 取得原有变量的值。
  2. 计算i-1。
  3. 对i进行赋值。

在这三个步骤中,若是有多个线程同时访问,那么必定会出现非线程安全问题。

解决方法就是使用 synchronized 同步关键字 使各个线程排队执行run()方法。修改后的run()方法:

public class MyThread implements Runnable {
    private int count = 5;

    @Override
    synchronized public void run() {
        count--;
        System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count);
    }
}

打印结果:

线程B 计算 count = 4
线程C 计算 count = 3
线程A 计算 count = 2
线程E 计算 count = 1
线程D 计算 count = 0

关于System.out.println()方法

先来看System.out.println()方法源码:

public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

虽然println()方法内部使用 synchronized 关键字,但以下所示的代码在执行时仍是有可能出现非线程安全问题的。

System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count--);

缘由在于println()方法内部同步,但 i-- 操做倒是在进入 println()以前发生的,因此有发生非线程安全问题的几率。

四、多线程方法

1. currentThread()方法

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

Thread.currentThread().getName()

2. isAlive()方法

方法isAlive()的功能是判断当前的线程是否处于活动状态。

thread.isAlive();

3. sleep()方法

方法sleep()的做用是在指定的毫秒数内让当前"正在执行的线程"休眠(暂停执行)。这个"正在执行的线程"是指this.currentThread()返回的线程。

Thread.sleep()

4. getId()方法

getId()方法的做用是取得线程的惟一标识。

thread.getId()

五、中止线程

中止线程是在多线程开发时很重要的技术点。中止线程并不像break语句那样干脆,须要一些技巧性的处理。

在Java中有如下3种方法能够终止正在运行的线程:

1)使用退出标志,使线程正常退出,也就是当run()方法完成后线程中止。

2)使用stop()方法强行终止线程,可是不推荐使用这个方法,由于该方法已经做废过时,使用后可能产生不可预料的结果。

3)使用interrupt()方法中断线程。

1.暴力法中止线程

调用stop()方法时会抛出 java.lang.ThreadDeath 异常,但在一般的状况下,此异常不须要显示地捕捉。

try {
            myThread.stop();
        } catch (ThreadDeath e) {
            e.printStackTrace();
        }

方法stop()已经被做废,由于若是强制让线程中止线程则有可能使一些清理性的工做得不到完成。另一个状况就是对锁定的对象进行了“解锁”,致使数据得不到同步的处理,出现数据不一致的状况。示例以下:

public class UserPass {
    private String username = "aa";
    private String password = "AA";

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    synchronized public void println(String username, String password){
        this.username = username;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
    }

    public static void main(String[] args) throws InterruptedException {
        UserPass userPass = new UserPass();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                userPass.println("bb","BB");
            }
        });
        thread.start();
        Thread.sleep(500);
        thread.stop();
        System.out.println(userPass.getUsername()+" "+userPass.getPassword());
    }

}

运行结果:

bb AA

2.异常法中止线程

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

那咱们如何判断该线程是否被打上了中止标记,Thread类提供了两种方法。

interrupted() 测试当前线程是否已经中断。
isInterrupted() 测试线程是否已经中断。

interrupted() 方法 不止能够判断当前线程是否已经中断,并且能够会清除该线程的中断状态。而对于isInterrupted() 方法,只会判断当前线程是否已经中断,不会清除线程的中断状态。

仅靠上面的两个方法能够经过while(!this.isInterrupted()){}对代码进行控制,但若是循环外还有其它语句,程序仍是会继续运行的。这时能够抛出异常从而使线程完全中止。示例以下:

public class MyThread extends Thread {
    @Override
    public void run() {
        try {
            for (int i=0; i<50000; i++){
                if (this.isInterrupted()) {
                    System.out.println("已是中止状态了!");
                    throw new InterruptedException();
                }
                System.out.println(i);
            }
            System.out.println("不抛出异常,我会被执行的哦!");
        } catch (Exception e) {
//            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(100);
        myThread.interrupt();
    }
    
}

打印结果:

...
2490
2491
2492
2493
已是中止状态了!

注意

若是线程在sleep()状态下被中止,也就是线程对象的run()方法含有sleep()方法,在此期间又执行了thread.interrupt() 方法,则会抛出java.lang.InterruptedException: sleep interrupted异常,提示休眠被中断。

3.return法中止线程

return法很简单,只须要把异常法中的抛出异常更改成return便可。代码以下:

public class MyThread extends Thread {
    @Override
    public void run() {
        
        for (int i=0; i<50000; i++){
            if (this.isInterrupted()) {
                System.out.println("已是中止状态了!");
                return;//替换此处
            }
            System.out.println(i);
        }
        System.out.println("不进行return,我会被执行的哦!");
       
    }
}

不过仍是建议使用“抛异常”来实现线程的中止,由于在catch块中能够对异常的信息进行相关的处理,并且使用异常能更好、更方便的控制程序的运行流程,不至于代码中出现多个return,形成污染。

六、暂停线程

暂停线程意味着此线程还能够恢复运行。在Java多线程中,可使用 suspend() 方法暂停线程,使用 resume()方法恢复线程的执行。

这俩方法已经和stop()同样都被弃用了,由于若是使用不当,极易形成公共的同步对象的独占,使得其余线程没法访问公共同步对象。示例以下:

public class MyThread extends Thread {
    private Integer i = 0;

    @Override
    public void run() {
        while (true) {
            i++;
            System.out.println(i);
        }
    }

    public Integer getI() {
        return i;
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(100);
        myThread.suspend();
        System.out.println("main end");
    }
    
}

打印结果:

...
3398
3399
3400
3401

执行上段程序永远不会打印main end。出现这样的缘由是,当程序运行到 println() 方法内部中止时,PrintStream对象同步锁未被释放。方法 println() 源代码以下:

public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

这致使当前PrintStream对象的println() 方法一直呈“暂停”状态,而且锁未被myThread线程释放,而主线程中的代码System.out.println("main end") 还在傻傻的排队等待,致使迟迟不能运行打印。


使用 suspend() 和 resume() 方法也容易由于线程的暂停而致使数据不一样步的状况,示例以下:

public class UserPass2 {
    private String username = "aa";
    private String password = "AA";

    public String getUsername() {
        return username;
    }


    public String getPassword() {
        return password;
    }


    public void setValue(String username, String password){
        this.username = username;
        if (Thread.currentThread().getName().equals("a")) {
            Thread.currentThread().suspend();
        }
        this.password = password;
    }

    public static void main(String[] args) throws InterruptedException {
        UserPass2 userPass = new UserPass2();
        new Thread(new Runnable() {
            @Override
            public void run() {
                userPass.setValue("bb","BB");
            }
        },"a").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(userPass.getUsername()+" "+userPass.getPassword());
            }
        },"b").start();

    }

}

打印结果:

bb AA

七、yield()方法

yield() 方法的做用是放弃当前的CPU资源,将它让给其余的任务去占用CPU执行时间。但放弃的时间不肯定,有可能刚刚放弃,立刻又得到CPU时间片。

public static void yield()  暂停当前正在执行的线程对象,并执行其余线程。

八、线程的优先级

在操做系统中,线程能够划分优先级,优先级较高的线程获得的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。

设置线程优先级有助于帮“线程规划器”肯定在下一次选择哪个线程来优先执行。

设置线程优先级使用setPriority()方法,此方法的JDK源码以下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

在Java中,线程优先级划分为1 ~ 10 这10个等级,若是小于1或大于10,则JDK抛出异常。

从JDK定义的3个优先级常量可知,线程优先级默认为5。

public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;

线程优先级具备继承性,好比A线程启动B线程,则B线程的优先级与A是同样的。

线程优先级具备规则性,线程的优先级与在代码中执行start()方法的顺序无关,与优先级大小有关。

线程优先级具备随机性,CPU尽可能使线程优先级较高的先执行完,但没法百分百确定。也就是说,线程优先级较高的不必定比线程优先级较低的先执行。

九、守护线程

在Java中有两种线程,一种是用户线程,一种守护线程。

什么是守护线程?守护线程是一种特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有了存在的必要了,自动销毁。能够简单地说:任何一个守护线程都是非守护线程的保姆。

如何设置守护线程?经过Thread.setDaemon(false)设置为用户线程,经过Thread.setDaemon(true)设置为守护线程。若是不设置属性,默认为用户线程。

thread.setDaemon(true);

示例以下:

public class MyThread extends Thread {
    private int i = 0;
    @Override
    public void run() {
        try {
            while (true){
                i++;
                System.out.println("i="+i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(5000);
        System.out.println("我离开后thread对象也就再也不打印了");
    }
}

打印结果:

i=1
i=2
i=3
i=4
i=5
我离开后thread对象也就再也不打印了

参考与总结

《Java多线程编程核心技术》高洪岩 著

本文主要介绍了Thread类的API,算是为学习多线程更深层次知识打下一些基础,文章如有错误请在评论区指正。

扩展

Java多线程编程核心技术(二)对象及变量的并发访问

Java多线程编程核心技术(三)多线程通讯

Java多线程核心技术(四)Lock的使用

Java多线程核心技术(五)单例模式与多线程

Java多线程核心技术(六)线程组与线程异常

相关文章
相关标签/搜索