Java多线程核心技术-01Java多线程基础

1 多线程基础

1.1 继承Thread类

        Java的JDK开发包已经自带了对多线程技术的支持,可以方便地进行多线程编程。实现多线程编程的方式主要有两种:一种是继承Thread类,一种是实现Runnable接口。

        在学习如何创建新的线程前,先来看看Thread类的声明结构,代码如下:

public class Thread implements Runnable 

        从上面的源码可以发现,Thread类实现了Runnable接口,它们之间具有多态关系。其实使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为Java语言的特点就是单继承,所以为了支持多继承,可以通过实现Runnable的方式。

        【示例1】创建自定义的线程类

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("my thread");
    }
}

在main()方法中,执行start方法。

public class RunMyThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println("运行结束!");
    }
}

       

        上面的代码使用start()方法来启动一个线程。线程启动后会自动调用线程对象中的run()方法,run()方法里面的代码就是线程对象要执行的任务,是线程执行任务的入口。从上面的运行结果来看,run()方法的执行时间晚于main()方法的执行时间,因为start()方法比较耗时,也增加了先输出main()方法的概率。

        start()耗时多的原因是内部执行了多个步骤:

        1、通过JVM告诉操作系统创建Thread.

        2、操作系统开辟内存并使用Windows SDK中的createThread()函数创建Thread线程对象。

        3、操作系统对Thread对象进行调度,已确定执行时机。

        4、Thread在操作系统中被执行成功。

        使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序无关。另外,线程是一个子任务,CPU是以不确定的方式或者随机的时间来调用线程的run方法。

1.2 线程随机性

        【示例】创建自定义线程类:

public class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 10000; i++) {
            System.out.println("run = " + Thread.currentThread().getName());
        }
    }
}

public class RunMyThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("myThread");
        myThread.start();
        for (int i = 0; i < 10000; i++) {
            System.out.println("main = " + Thread.currentThread().getName());
        }
        System.out.println("运行结束!");
    }
}

        多线程随机输出的原因是CPU将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替执行并输出,导致输出结果成乱序。

        时间片就是CPU分配给各个线程的时间。每个线程被分配一个时间片,在当前的时间片内执行线程的任务。需要注意的是,当CPU在不同的线程上进行切换时是需要耗时的,所以并不是创建的线程越多,执行的效率就越快。想法,线程数过多反而会降低代码的执行效率。

        如果调用“run()”,而不是调用“start()” ,就不是异步执行,而是同步执行,那么此线程对象并不交给线程规划器来处理,而是由main线程来调用run方法,也就是必须等run方法执行完才可以执行后面的代码。

1.3 实现Runnable接口

        【示例】创建一个实现Runnable接口的MyRunnable类:

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("运行中");
    }
}
public class RunRunnable {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println("运行结束");
    }
}

1.4 使用Runnable的优点

        使用继承Thread类的方式来开发多线程是有局限性的,因为Java是单继承,不支持多继承,为了改变这种限制,可以使用实现Runnable接口的方式来实现多线程。 

        使用Runnable接口方式实现多线程可以把”线程“和”任务“分离,Thread代表线程,Runnable代表可以运行的任务。

1.5 实例变量共享导致非线程安全的问题

        自定义线程类中的实例变量针对其他线程有共享和不共享之分,这在多个线程之间交互时是很重要的。

        1、不共享数据的情况

        【示例1.5.1】     

public class MyThread1 extends Thread {

    private int count = 5;
    public MyThread1(String name){
        super();
        this.setName(name);
    }
    @Override
    public void run(){
        super.run();
        while(count > 0){
            count --;
            System.out.println("由" + Thread.currentThread().getName() + "计算 count = " + count );
        }
    }
}

       

public class MyThreadMain  {
    public static void main(String[] args) {
        MyThread1 thread1 = new MyThread1("a");
        MyThread1 thread2 = new MyThread1("b");
        MyThread1 thread3 = new MyThread1("c");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

 

        分析:一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值,这样的情况就是变量不共享。

         2、共享数据的情况

        共享数据的情况就是多个线程可以访问一个变量。

        【示例1.5.2】

public class MyThread2 extends Thread{
    private int count = 5;
    @Override
    public void run(){
        super.run();
        count --;
        System.out.println("由" + Thread.currentThread().getName() + "计算 count = " + count );
    }
}
public class MyThread2Main {
    public static void main(String[] args) {
        MyThread2 mythread = new MyThread2();
        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();
    }
}

分析:从上图可以看到,线程A B C输出的值都是2,说明A B C同时对count进行处理,产生了分线程安全问题。

        出现非线程安全的情况是因为某些jvm中,count--的操作要分解成如下三步(执行这三个步骤的过程中会被其他线程所打断)

        1、取得原来count的值。

        2、计算count - 1

        3、对count 进行重新赋值。

        在这三个步骤中,如果有多个线程同时访问,那么很大概率会出现非线程安全问题。

        针对上面出现线程不安全的情况下,在run方法前加上synchronized关键词,就不会出现非线程安全的情况了。

public class MyThread2 extends Thread{
    private int count = 5;
    @Override
    synchronized  public void run(){
        super.run();
        count --;
        System.out.println("由" + Thread.currentThread().getName() + "计算 count = " + count );
    }
}

 分析:在run方法的前面假如synchronized关键字,使多个线程在执行run方法时,一排队的方式进行处理。一个线程在调用run方法前,需要先判断run方法有没有上锁,如果上锁,说明有其他线程正在执行run方法,必须等其他线程执行结束后才可以执行run方法,这样也就实现了排队调用run方法的目的,实现了顺序对count变量-1的效果。虽然count -- 仍然有3个步骤,但在执行这三个步骤时并没有被打断,呈”原子性“,所以运行结果是正确的。

        使用synchronized关键字修饰的方法称为”同步方法“,可用来对方法内部的全部代码执行加锁,而加锁的这段代码称为”互斥区“或”临界区“。

        当一个线程想要执行同步方法里的代码时,它会先尝试拿这把锁,如果能够拿到,那么该线程就会执行synchronized里面的代码,如果拿不到,那么这个线程就会不断尝试去拿这把锁,直至拿到为止。

2 常用方法

2.1 currentThread()方法

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

2.2 isAlive()方法

        判断线程对象是否存活。

public class MyThread1 extends Thread{
    @Override
    public void run(){
        System.out.println("run = " + this.isAlive());
    }
}
public class MyThread1Main {
    public static void main(String[] args) {
        MyThread1 t = new MyThread1();
        System.out.println("begin = " + t.isAlive());
        t.start();
        System.out.println("end = " + t.isAlive());
    }
}

        isAlive()的作用是测试线程是否处于活动状态。那么什么事活动状态呢?即线程已经启动尚未终止。如果线程处于正在运行或准备开始运行的状态,就认为线程是”存活“的。需要说明一下,对于代码:

System.out.println("end = " + t.isAlive());

虽然输出的是true,但此值是不确定的。输出true值时因为t线程还未执行完毕。

2.2 StackTraceElement[] getStackTrace()

public StackTraceElement[] getStackTrace()方法的作用是返回一个表示该线程的堆栈跟踪元素数组。如果该线程尚未启动或已经终止,则该方法将返回一个零长度数组。如果返回的数组长度不为零,则第一个元素代表堆栈顶,它是该数组中最新的方法调用。最后一个元素代表堆栈底,是该数组中最旧的方法调用。

【示例2.2】

public class Test1 {
    public void a(){
        b();
    }
    public void b(){
        c();
    }

    public void c(){
        d();
    }

    public void d(){
        e();
    }
    public  void e(){
        StackTraceElement[] array = Thread.currentThread().getStackTrace();
        if(array != null){
            for (int i = 0; i < array.length; i++) {
                StackTraceElement stackTraceElement = array[i];
                System.out.println("className = " + stackTraceElement.getClassName() +
                        "methodName = " + stackTraceElement.getMethodName() +
                        "fileName = " + stackTraceElement.getFileName() +
                        "lineNumber = " + stackTraceElement.getLineNumber());
            }
        }
    }

    public static void main(String[] args) {
        Test1 test1 = new Test1();
        test1.a();

    }

}

3 停止线程

        停止线程多线程开发的一个重要的技术点。在大多数情况下,使用Thread.interrupt()方法停止一个线程。

~未完待续,感谢阅读

你可能感兴趣的:(java,java,jvm,开发语言)