Java多线程和并发-第一篇(基础篇)

1.进程线程的由来

最初的计算机只能接收一些特定的指令,用户输入一个指令以后,计算机才操作。然后出现了批处理操作系统,把一系列的操作指令写在一个清单上,一次性交给计算机,但是由于批处理操作系统的指令运⾏⽅式仍然是串⾏的,内存中始终只有⼀个程序在运⾏,后⾯的程序需要等待 前⾯的程序执⾏完成后才能开始执⾏,⽽前⾯的程序有时会由于I/O操作、⽹络等 原因阻塞,所以批处理操作效率也不⾼。

1.1 进程的由来

进程就是应⽤程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不⼲扰。同时进程保存着程序每⼀个时刻运⾏的状态。

运行机制
CPU采⽤时间⽚轮转的⽅式运⾏进程:CPU为每个进程分配⼀个时间段,称作它的时间⽚。如果在时间⽚结束时进程还在运⾏,则暂停这个进程的运⾏,并且 CPU分配给另⼀个进程(这个过程叫做上下文切换)。如果进程在时间⽚结束前阻塞或结束,则CPU⽴即进⾏切换,不⽤等待时间⽚⽤完。使用方式,进程让操作系统的并发成为了可能

上下文切换:(有时也称做进程切换或任务切换)是指 CPU 从⼀个进程(或线程) 切换到另⼀个进程(或线程)。上下⽂是指某⼀时间点 CPU 寄存器和程序计数器的内容。但是在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,所以任务从保存到加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的,意味着此操作系统会消耗大量的CPU时间,故线程也不是越多越好。

1.2线程的由来

让⼀个线程执⾏⼀个⼦任务,这样⼀个进程就包含了多个线程,每个线程负责⼀个单独的⼦任务。线程让进程的内部并发成为了可能。
总之,上面的目的都是提高操作系统的效率。

1.3进程与线程的区别(可以参考富士康厂房和流水线)

流水线:富士康按照厂房(进程)分配所需要的原料,每个厂房有几条生产线(线程),每个生产线上有很多工人(协程)。
进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即,CPU分配时间的单位。
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):

。进程有单独的内存地址空间,所以程序间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。

。进程单独占有⼀定的内存地址空间,⼀个进程出现问题不会影响其他进程,不 影响主程序的稳定性,可靠性⾼;⼀个线程崩溃可能影响整个程序的稳定性, 可靠性较低。

。进程单独占有⼀定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及⻚调度,开销较⼤;线程只需要保存寄存 器和栈信息,开销较⼩。

2.Java多线程入门类和接口

2.1Thread类和Runnable接口

如何使用多线程?
首先,我们需要有一个“线程”类。JDK提供了Thread类和Runnalble接口来让我们实现自己的“线程类”。
。继承Thread类,并重写run方法;
。实现Runable接口的run方法;

2.1.1继承Thread类

首先是继承Thread类:

public class Demo1 {
    
    
    public static class MyThread extends Thread{
    
    
        @Override
        public void run() {
    
    
            System.out.println("MyThread");
        }
    }
    /*
    1.我们在程序里面调用start()方法后,虚拟机会为我们创建一个线程,然后等到
    这个线程第一次得到时间片时再调用run()方法。
    2.注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()
    方法会抛出异常。
     */
    public static void main(String[] args) {
    
    
        Thread myThread=new MyThread();
        myThread.start();
    }
}

注意:要调用start()方法后,该线程才算启动!

Thread几个常用的方法
currentThread():静态方法,返回对当前正在执行的线程对象的引用。

start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;

yield():yield放弃的意思,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续执行这个线程的;

sleep():静态方法,使当前线程睡眠一段时间;

join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的使Object类的wait方法实现的;

2.1.2Runnable接口

Runnable函数式接口

@FunctionalInterface 
public interface Runnable {
    
         
public abstract void run(); 
}

示例代码:

public class Demo2 {
    
    
    public  static class MyThread implements  Runnable{
    
    
        @Override
        public void run() {
    
    
            System.out.println("MyThread");
        }
    }
    public static void main(String[] args) {
    
    
        //创建自定义类对象,线程任务对象
        MyThread mr= new MyThread();
        //创建线程对象
        Thread t=new Thread(mr);
        t.start();
        
        //Java 8函数式编程,可以省略MyThread类
        new Thread(()->{
    
    
            System.out.println("Java 8 匿名内部类");
        }).start();

    }
}

2.2Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
(1)适合多个相同的程序代码的线程去共享一个资源。
(2)可以避免java中单继承的局限性。
(3)增加线程的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
(4)线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

注意:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

2.3匿名内部类方式实现线程的创建

使用线程的匿名内部类方法,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法;

public class Demo3
{
    
    
    public static void main(String[] args) {
    
    
        Runnable r=new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("你好");
            }
        };
        new Thread(r).start();
    }
}

3.线程组和线程优先级

3.1线程组(ThreadGroup)

Java中⽤ThreadGroup来表示线程组,我们可以使⽤线程组对线程进⾏批量控制。
ThreadGroupThread的关系就如同他们的字⾯意思⼀样简单粗暴,每个Thread必然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。
执⾏main() ⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组.

示例代码:

public class Demo4 {
    
    
    public static void main(String[] args) {
    
    
        Thread testThread = new Thread(()->{
    
    
            System.out.println("testThread当前线程组名字:"+Thread.currentThread().getThreadGroup().getName());
            System.out.println("testThread线程名字:"+Thread.currentThread().getName());
        });
        testThread.start();
        System.out.println("执行main方法线程名字:"+Thread.currentThread().getName());
    }
}

输出结果

执行main方法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0

3.2 线程优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10 级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。

Java默认的线程优先级为5,线程的执⾏顺序由调度程序来决定,线程的优先级会在线程被调用之前设定通常情况下,高优先级的线程将会比低优先级的线程有更高的机率得到执行。线程的优先级在创建线程时可以设置,也可以使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级,getPriority()方法获取线程的优先级。

Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统⼀个建议,操作系统不⼀定会采纳。而真正的调⽤顺序,是由操作系统的线程调度算法决定的。

如果某个线程优先级大于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。

4.Java线程状态及主要转化方式

4.1操作系统中的线程状态转换

在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态保持一致的。
在这里插入图片描述
主要的三个状态:
(1)就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
(2)执行状态(running):线程正在使用CPU。
(3)等待状态(waiting):线程经过等待事件的调用或者正在等待其他资源(I/O).

4.2 线程生命周期经理的5种状态

(1)新建状态(New):线程创建后处于该状态;
(2)可运行状态(Runnable):新建的线程调用start( )方法,将使线程的状态从New转换为Runnable;
(3)运行状态(Running):运行状态使线程占有CPU并实际运行的状态;
(4)阻塞状态(Blocked):导致该状态的原因很多,注意区别;
(5)终止状态(Dead):线程执行结束的状态,没有任何方法可改变它的状态。

5. Java线程的通信

5.1 锁与同步

在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得到这个线程和这个锁“离婚”(释放)。

5.1.1 线程同步

什么是同步呢,假如我们现在有2位正在 抄暑假作业答案的同学:线程A和线程B。当他们正在抄的时候,⽼师突然来修改了⼀些答案,可能A和B最后写出的暑假作业就不⼀样。我们为了让A,B能写出2本相 同的暑假作业,我们就需要让⽼师先修改答案,然后A,B同学再抄。或者A,B同 学先抄完,⽼师再修改答案。这就是线程A,线程B的线程同步。
可以以解释为:线程同步是线程之间按照⼀定的顺序执行。

5.1.2 进程同步

进程同步是指进程之间一种直接的协同工作关系,这些进程相互合作,共同完成一项任务。进程间的直接相互作用构成进程的同步。
为了达到线程同步,我们可以使用锁来实现它。
无锁案例

public class NoneLock {
    
    
    static class ThreadA implements Runnable{
    
    
        @Override
        public void run() {
    
    
            for(int i=0;i<100;i++){
    
    
                System.out.println("Thread A"+i);
            }
        }
    }
    static class ThreadB implements Runnable{
    
    
        @Override
        public void run() {
    
    
            for (int i=0;i<100;i++){
    
    
                System.out.println("Thread B"+i);
            }
        }
    }

    public static void main(String[] args) {
    
    
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
}

每次运行结果会不同。

加锁案例:

public class ObjectLock {
    
    
    private static Object lock = new Object();
    static class ThreadA implements Runnable{
    
    
        @Override
        public void run() {
    
    
            synchronized (lock){
    
    
                for(int i=0; i<100; i++){
    
    
                    System.out.println("ThreadA"+i);
                }
            }
        }
    }
    static class ThreadB implements Runnable{
    
    
        @Override
        public void run() {
    
    
            synchronized (lock){
    
    
                for (int i=0; i<100; i++){
    
    
                    System.out.println("Thread B"+i);
                }
            }
        }
    }

    public static void main(String[] args) {
    
    
        new Thread(new ThreadA()).start();
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

每次输出的结果都是顺序的。
这里声明了一个名字为lock的对象锁。我们ThreadA和ThreadB内需要同步的代码块里,都是synchronized关键字加上了同一个对象锁lock
根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么 线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。

5.2等待/通知机制

上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务资源。
而等待/通知机制是另一种方式。
Java多线程的等待/通知机制是基于Object类的wait()方法和notify(),notifyAll()方法来实现的.

notify()⽅法会随机叫醒⼀个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。

前⾯我们讲到,⼀个锁同⼀时刻只能被⼀个线程持有。⽽假如线程A现在持有了⼀ 个锁 lock 并开始执⾏,它可以使⽤ lock.wait() 让⾃⼰进⼊等待状态。这个时 候,lock 这个锁是被释放了的。
这时,线程B获得了 lock 这个锁并开始执⾏,它可以在某⼀时刻,使⽤ lock.notify() ,通知之前持有 lock 锁并进⼊等待状态的线程A,说“线程A你不⽤等了,可以往下执⾏了”。

示例代码:

public class WaitAndNotify {
    
    
    private static Object lock=new Object();
    static class ThreadA implements Runnable{
    
    
        @Override
        public void run() {
    
    
            synchronized(lock){
    
    
                for(int i=0;i<100;i++){
    
    
                    try {
    
    
                        System.out.println("ThreadA"+i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }
    static class ThreadB implements Runnable{
    
    
        @Override
        public void run() {
    
    
            synchronized (lock){
    
    
                for (int i=0;i<100;i++){
    
    
                    try {
    
    
                        System.out.println("ThreadB"+i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

运行结果:

ThreadA0
ThreadB0
ThreadA1
ThreadB1
ThreadA2
ThreadB2
ThreadA3

在这个Demo⾥,线程A和线程B⾸先打印出⾃⼰需要的东⻄,然后使⽤ notify() ⽅法叫醒另⼀个正在等待的线程,然后⾃⼰使⽤ wait() ⽅法陷⼊等待 并释放 lock 锁。
注意:
需要注意的是等待/通知机制使⽤的是同⼀个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能⽤等待/通知机制通信的。

5.3信号量

JDK提供了⼀个类似于“信号量”功能的类 Semaphore 。但这里不是要介绍这个类,而是介绍⼀种基于 volatile 关键字的⾃⼰实现的信号量通信。
volitile关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变 量,在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。
如何让A与B交替输出数据呢?
示例代码:

public class Signal {
    
    
    private static volatile int signal=0;
    static class  ThreadA implements Runnable{
    
    
        @Override
        public void run() {
    
    
            while (signal<5){
    
    
                if(signal%2==0){
    
    
                    System.out.println("ThreadA:"+signal);
                    synchronized (this){
    
    
                        signal++;
                    }
                }
            }
        }
    }
    static class ThreadB implements Runnable{
    
    
        @Override
        public void run() {
    
    
            while (signal<5){
    
    
                if(signal%2==1){
    
    
                    System.out.println("ThreadB:"+signal);
                    synchronized (this){
    
    
                        signal++;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

输入结果:

ThreadA:0
ThreadB:1
ThreadA:2
ThreadB:3
ThreadA:4

我们可以看到,使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥需要要注意的是,volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操作,所以我们需要使⽤ synchronized 给它“上锁”。
原子操作:就是无法被别的线程打断的操作,要么不执行,要么执行成功。

5.4 管道

管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、PipedReader 、PipedOutputStream、PipedInputStream 。其中,前⾯两个是基于字符的,后⾯两个是基于字节流的。
Java管道:Java/IO系统是建立再数据流概念之上的,它具有将一个程序的输出当作另一个程序的输入。

5.5 join方法

join()⽅法是Thread类的⼀个实例⽅法。它的作⽤是让当前线程陷⼊“等待”状态,等 join的这个线程执⾏完成后,再继续执⾏当前线程。
有时候,主线程创建并启动了⼦线程,如果⼦线程中需要进⾏⼤量的耗时运算,主线程往往将早于⼦线程结束之前结束。
如果主线程想等待⼦线程执⾏完毕后,获得⼦线程中的处理完的某个数据,就要⽤到join⽅法了。

示例代码:

public class Join {
    
    
    static class ThreadA implements Runnable{
    
    
        @Override
        public void run() {
    
    
            try {
    
    
                System.out.println("我是子线程,我先睡一秒");
                Thread.sleep(1000);
                System.out.println("我是子线程,我睡完了一秒");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread=new Thread(new ThreadA());
        thread.start();
        thread.join();
        System.out.println("如果不加join⽅法,我会先被打出来,加了就不⼀样了");
    }
}

5.6 sleep方法

sleep⽅法是Thread类的⼀个静态⽅法。它的作⽤是让当前线程睡眠⼀段时间。它有这样两个⽅法:
这⾥需要强调⼀下:sleep⽅法是不会释放当前的锁的而wait方法会。这也是最常见的⼀个多线程⾯试题。
sleep跟wait方法的区别
(1)wait可以指定时间,也可以不指定;而sleep必须指定时间。
(2)wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
(3)wait必须放在同步块或同步方法中,而sleep可以任意位置。

猜你喜欢

转载自blog.csdn.net/zhanlong11/article/details/114685457