JAVA高级(一)------ 多线程编程

目录

一、进程与线程

二、线程运行状态

三、多线程的代码实现

(1)方式一:继承Thread类

(2)方式二:实现Runnable接口

(3)Callable实现多线程

四、Thread、Runnable、Callable区别与联系

五、多线程常见方法

(1)线程命名与获取

(2)线程的休眠

(3)线程的强制执行

(3)线程的礼让

(4)线程的优先级

六、线程停止


Java是支持多线程的编程语言,多线程是相对于单线程(单进程)而言的,传统的DOS系统是单进程的,同一时间段只允许一个进程执行,当出现病毒那么将导致整个系统瘫痪。多线程则允许同一个时间段多个程序轮流运行,轮流抢占CPU资源。

一、进程与线程

线程是在进程的基础上划分的更小的程序单元,线程是在进程的基础上创建并使用的,所以线程依赖进程的支持,但是线程的启动速度要比进程快许多,当时用多线程进行并发处理的时候,执行性能高于进程。

区别:

  1. 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元;
  2. 同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程;
  3. 进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束;
  4. 线程是轻两级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的;
  5. 线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源;
  6. 线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志。

进程与线程的选择取决以下几点:

  1. 需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的;
  2. 线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应;
  3. 因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
  4. 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
  5. 需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

二、线程运行状态

 

  1. 任何的线程对象都是用Thread类进行封装的,且start()后进入就绪状态,但未执行;
  2. 就绪状态的线程当CPU资源调度到它时进入r运行状态;
  3. 进入执行状态的线程对象可能不会一直执行到结束,例如当它让出资源后会进入阻塞状态,阻塞解除后重新进入就绪状态等待CUP调度;
  4. run()方法执行完毕则进入终止状态。

主方法(main()方法)也是一个线程,实际开发中,主线程可以创建若干子线程,让子线程去处理复杂或者耗时的业务

三、多线程的代码实现

实现多线程有3种方式:

  • (1)方式一:继承Thread类

Java提供了一个java.lang.Thread的程序类,继承了这个类的子类并覆写run()方法才能实现多线程处理,如下:

public class ThreadTest extends Thread{
    private String name;
    public ThreadTest(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int x = 0; x < 10; x++){
            System.out.println(this.name + "-"+ x);
        }
    }
}

class Test{
    public static void main(String[] args) {
        new ThreadTest("线程1").start();
        new ThreadTest("线程2").start();
        new ThreadTest("线程3").start();
    }
}
线程1-0
线程1-1
线程1-2
线程1-3
线程2-0
线程2-1
线程2-2
线程2-3
线程2-4
线程3-0
线程2-5
线程2-6
线程2-7
线程2-8
线程2-9
线程1-4
线程1-5
线程1-6
线程1-7
线程1-8
线程1-9
线程3-1
线程3-2
线程3-3
线程3-4
线程3-5
线程3-6
线程3-7
线程3-8
线程3-9

可以看出,执行并非按照顺序执行,而是交替执行,说明已经进行了多线程的处理。需要注意的是,启动线程不是直接使用run()方法,而是通过start()方法完成的,且需要注意的是每一个线程对象只允许启动一次

  • (2)方式二:实现Runnable接口

class DemoThread implements Runnable{
    private String content;
    public DemoThread(String content) {
        this.content = content;
    }
    public DemoThread() {
    }

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

public class RunnableTest {
    public static void main(String[] args) {
        DemoThread demoThread1 = new DemoThread("线程1");
        DemoThread demoThread2 = new DemoThread();
        Thread t1 = new Thread(demoThread1);
        Thread t2 = new Thread(demoThread2, "线程2");
        t1.start();
        t2.start();
    }
}
线程2-0
Thread-0-0
线程2-1
Thread-0-1
Thread-0-2
Thread-0-3
线程2-2
Thread-0-4
线程2-3
Thread-0-5
线程2-4
Thread-0-6
线程2-5
Thread-0-7
线程2-6
线程2-7
线程2-8
线程2-9
Thread-0-8
Thread-0-9

Runnable这种方式,因为不用继承Thread类,不再有单继承的局限。因此,推荐使用Runnable这种方式实现多线程。

此外,JDK1.8开始,Runnable这种方式还可以利用Lambda表达式进行多线程的实现

public class LambdaTest {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            String name = "线程-" + i;
            Runnable run = ()->{
                for (int j = 0; j < 10; j++) {
                    System.out.println(name + " " + j);
                }
            };
            new Thread(run).start();
        }
    }
}
线程-0 0
线程-1 0
线程-1 1
线程-1 2
线程-1 3
线程-2 0
线程-1 4
线程-2 1
线程-1 5
线程-2 2
线程-1 6
线程-2 3
线程-1 7
线程-2 4
线程-0 1
线程-0 2
线程-0 3
线程-0 4
线程-0 5
线程-0 6
线程-1 8
线程-0 7
线程-2 5
线程-0 8
线程-1 9
线程-0 9
线程-2 6
线程-2 7
线程-2 8
线程-2 9
  • (3)Callable实现多线程

class DThread implements Callable{

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        System.out.println(sum);
        return sum;
    }
}

public class CallableTest {
    public static void main(String[] args) throws Exception{
        FutureTask task = new FutureTask(new DThread());
        new Thread(task).start();
        System.out.println(task.get());
    }
}
705082704
705082704

可以看出,Callable这种方式可以有返回值,这是Runnable方式所不具备的。且Callable还支持泛型。

四、Thread、Runnable、Callable区别与联系

(1)Thread和Runnable的关系

  • Runnable可以避免单继承局限,更好的进行功能扩充;
  • Thread类是Runnable接口的子类,继承Thread类实现的run()方法其实是Runnable的run()方法

(2)Runnable与Callable的区别

  • Runnable出现的较早(JDK1.0),Callable出现较晚(JDK1.5)
  • Runnable提供run()方法,没有返回值;
  • Callable提供call(),有返回值。

五、多线程常见方法

(1)线程命名与获取

  • 构造方法命名:Thread(Runnable target, String name)
  • 设置命名:setName(String name)
  • 获取线程名:getName(String name)
  • 获取当前线程名称:currentThread()

(2)线程的休眠

如果希望一个线程暂缓执行就需要使用休眠处理,通过sleep()方法实现。休眠的主要特点就是可以自动实现线程的唤醒,继续后续处理。

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

睡眠过程中可能会报InteruptedException异常,因此sleep()方法必须强制处理或抛出异常。

除了sleep()方法,还有一个wait()方法,wait()方法也可以中断线程的运行,使本线程等待,暂时让出CPU的使用权,并允许其他线程使用这个同步方法

sleep和wait的区别:

① 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。

sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

② 锁: sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中,使得其他线程可以使用同步控制块或者方法。
 

sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。

③ 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

(3)线程的强制执行

正常情况下,线程之间是抢占资源轮流执行的,但当满足某些条件后,某个线程对象可以一直独占资源进行执行,这就是线程的强制执行。强制执行需要调用join()方法。不加join时:

class ThreadDemo extends Thread {
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=1; i<=6; i++){
            System.out.println(name+"-"+i);
        }
    }
}

public class JoinTest {
    public static void main(String[] args) {
        ThreadDemo t1 = new ThreadDemo("A");
        ThreadDemo t2 = new ThreadDemo("B");
        t1.start();
        t2.start();
    }
}
A-1
B-1
B-2
B-3
B-4
B-5
A-2
A-3
A-4
A-5

加了join后:

class ThreadDemo extends Thread {
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=1; i<=6; i++){
            System.out.println(name+"-"+i);
        }
    }
}

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t1 = new ThreadDemo("A");
        ThreadDemo t2 = new ThreadDemo("B");
        t1.start();
        t1.join();
        t2.start();
    }
}
A-1
A-2
A-3
A-4
A-5
A-6
B-1
B-2
B-3
B-4
B-5
B-6

可以看出,线程A会在调用join()后强制优先执行,完成后线程B才执行。

(3)线程的礼让

是指先将资源让出去让别的线程先执行。需要调用yield()方法。

class ThreadDemo extends Thread {
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=1; i<=6; i++){
            if(i % 2 == 0){
                Thread.yield();
            }
            System.out.println(name+"-"+i);
        }
    }
}

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t1 = new ThreadDemo("A");
        ThreadDemo t2 = new ThreadDemo("B");
        t1.start();
        t2.start();
    }
}
A-1
B-1
A-2
B-2
B-3
A-3
B-4
A-4
B-5
A-5
B-6
A-6

在能被2整除的位置,线程间会产生礼让,但是这种礼让不绝对,即使A线程已经礼让了,但CUP还是可能在下个执行片段调度到线程A。

(4)线程的优先级

线程的优先级表明线程抢占到资源的概率,优先级高则抢占到资源的概率越大。

有三种优先级,线程的优先级是1-10之间的正整数,线程优先级最高为10,最低为1,默认为5:

  • 1- MIN_PRIORITY
  • 10-MAX_PRIORITY
  • 5-NORM_PRIORITY

六、线程停止

停止某个线程有很多方法:

  • stop()
  • destroy()
  • suspend()

但以上3个方法已经因为可能会造成死锁,所以不建议直接使用。

要想停止某个线程,需要更加柔和的方式:

public class Demo2 {
    public static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            long num = 0;
            while(flag){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " " + num++);
            }
        }, "非主线程").start();
        Thread.sleep(200);
        flag = false;
    }
}
非主线程 0
非主线程 1
非主线程 2
非主线程 3
非主线程 4
非主线程 5
非主线程 6
非主线程 7
非主线程 8
非主线程 9

这种停止方式使得线程不会直接立即停下,而是慢慢的停下,更加柔和安全。

七、volatile关键字

volatile关键字的作用:当操作该volatile变量时,所有前序对该变量的操作都已完成(如不存在已变更,但未写回主存的情况),所有后续对该变量的操作,都未开始(保证变量的可见性)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。更抽象一点的说法是volatile关键字可以消除一些非原子操作带来的问题。

在很多线程安全的容器中有应用,在单例双检锁设计模式中也有应用,后面的文章会介绍。

发布了92 篇原创文章 · 获赞 3 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41231928/article/details/103074169