Java 多线程和并发编程(一)——附代码

引入

        先理清几个易混淆的概念:易混淆:并发、并行、互斥、同步、异步​​​​​​​,然后开始今天的学习吧~

        文中如果有不对的地方欢迎各位小伙伴们指出,我会及时进行更正!!

进程、多线程

        目前的CPU频率已经无法再有更大的提高空间,所以提高程序性能主要依靠多核和并行程序。而要了解 Java 的多线程,我们还得先来说一说多进程~

        需要指出的是,在引入线程之后,线程是独立调度的基本单位,进程是拥有资源的基本单位。不仅进程之间可以并发执行,线程之间也可以并发执行,这使得操作系统具有更好的并发性。

多进程

        我们都知道,操作系统(OS)会将时间划分为多个时间很短的时间片,在每个时间片内将CPU分配给某一个任务,当时间片结束,CPU将被自动回收,分配给另外的任务。

        在CPU上,任务是按照串行依次运行(单核CPU),如果是多核,多个任务进程可以并行。

多进程的优点

        - 可以同时运行多个任务;

        - 程序因 IO 堵塞,可以释放CPU,让CPU为其它程序服务;

        - 当系统有多个CPU,可以为多个程序同时服务

多进程的缺点

        - 进程切换带来的系统开销代价大,降低了整体性能;(假设一个进程需要频繁使用IO,那么在阻塞的过程中,就需要来回切换进程)

        - 进程之间的通信的实现也比较复杂,不好管理;

        - 进程的创建和撤销都需要一定的时间代价

多线程

        为了解决多进程带来的缺点,所以引进了多线程处理技术。我们可以通俗的类比于一个main()函数里面有很多内容,为此,我们在编写程序的时候,可以把它分为很多模块,一个模块就是一个函数。

        这里是一样的道理,一个程序可以包括多个子任务,多个子任务可串/并行,每个子任务称之为一个线程!

        如果一个子任务阻塞,CPU将会去调度另一个子任务进行工作,但CPU仍保留在本程序(进程)中,而不是被调度到别的程序(进程)去,这样就可以减少进程之间的切换从而大大提高本程序所获得CPU时间和利用率!

多进程和多线程对比

        从以下几个方面来看:

        多线程通讯更加高效,可以通过共享内存、消息队列等方式实现,而多进程间通信较为困难,需要采用IPC(进程间通信)技术;

        多线程内存开销更小,因为多线程用的是统一进程的内存空间,而多进程必须为每个进程分配内存空间;

        多线程的创建和销毁更迅速,因为线程创建只需要拷贝一份主程序的执行上下文,销毁时只需要回收相应资源即可,不需要操作系统介入,而多进程需要创建一个新的进程,销毁也需要释放多个进程所占用的内存资源 ,需要操作系统介入,增加了系统开销;

        多线程在访问共享资源时需要进行锁定,避免多个线程同时修改同一份数据,而多进程可以通过消息队列等方式进行同步,但是需要注意进程间的数据一致性问题。

多线程实现

多线程的创建

        在 Java 中进行多线程的创建,只有以下两种方式:

// 继承 Thread 类
public class Thread1 extends Thread{
    public void run()
    {
        Sytem.out.printlin("hello");
    }
}

// 实现 Runnable 接口
public class Thread2 implements Runnable{
    public void run()
    {
        System.out.println("hello");
    }
}

注:Java 的四个主要接口有:

        - Clonable :用于对象克隆 ;

        - Comparable:用于对象比较;

        - Serializable:用于对象序列化;

        - Runnable:用于对象线程化。

两种创建方式的比较

 注:Thread里面,必须用 static 定义变量 ,才能实现变量共享 

多线程的启动

        上述说的两种创建都属于run方法,且Java里规定, 调动start就会触发run方法,它的底层是用JNI(Java Native Interface)【这个我会在JVM的学习中谈到 】, 提供了若干个API,可以使得Java程序调用C/C++程序!(Java 就是用 C/C++写出来的~~)

        main 函数只是叫做主线程,其他新的派生出来的线程叫子线程,主线程终止了子线程可能还在,一定是得等到所有的线程都结束了,整个程序才算终止

// Thread
public class Thread1 extends Thread {

    public void run() {
        System.out.println("hello");
    }

    public static void main(String[] a) {
        (new Thread1()).start();
    }
}


// Runnable
public class Thread2 implements Runnable {

    public void run() {
        System.out.println("hello");
    }
    //实现 Runnable 的类必须要依靠 Thread 才可以启动
    //不能直接对 Runnable 的对象进行 start 方法
    public static void main(String[] a) {
        (new Thread(new Thread2())).start();
    }
}

多线程信息共享

        在介绍多线程信息共享之前先说两个基础概念——粗粒度和细粒度。

        - 粗粒度:子线程和子线程之间 和 main 线程之间缺乏交流

        - 细粒度:线程之间有信息交流通讯(即同步协作)

        那怎样才能让线程之间可以进行信息交流呢?

        首先得要有可以共享的信息!因为 Java 的 JDK 库中暂时是不支持点对点的发送消息的(即线程与线程之间直接通信)【这个在 C/C++ 里面有一个并行库 MPI 可以直接做到!】

信息共享的方法

        在 Java 中,达到信息共享的也有两种办法:

        - 通过共享变量(static)实现信息共享。static 变量是这个类的所有对象都共享的一个变量 

        - 同一个Runnable类的成员变量。这样的共享变量在多个线程中实际上就是一个拷贝对象了。

下面是两个分别使用两种创建方式的具体的例子:

//会出现信息不一致的情况
public class ThreadDemo0 {

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

class TestThread0 extends Thread {
    private int tickets = 100;  //每个线程卖100张,没有共享
    // private static int tickets = 100;  // static 变量是共享的,所有的线程共享
    public void run() {
        while(true){
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets);
                --tickets;
            } else {
                 break;
            }
        }
    }
}

注:如果一个类是通过继承Thread 类,那么它的信息共享只能通过static变量

//仍然会出现信息不一致的问题
public class ThreadDemo1 {

    public static void main(String[] args) {
        TestThread1 t = new TestThread1();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

class TestThread1 implements Runnable {
    private int tickets = 100;  
    public void run() {
        while(true){
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets);
                --tickets;
            } else {
                 break;
            }
        }
    }
}

        在上述例子中,TestThread1 只被创建一次,就是 t ,new Thread ( t ) 并没有创建新对象,只是将 t 包装成线程对象,然后启动 ,所以使用的是同一个TestThread1对象。

Java的内存模型

         在前面的信息共享中,我们已经实现了多线程,但存在线程之间得到数据不一致的问题,原因有两个,一是由于每个线程都有自己的工作缓存副本(见上图—Java 的内存模型),二是由于关键步骤缺乏加锁限制。

        - 工作缓存副本:线程运行时,会先从内存里面加载完数据,放到它自己的工作缓存里面,然后开始运算,运算完以后会将数据先写入到自己的线程缓存中,最后再更新到内存中。假设有一个线程修改了自己工作缓存的值,但是还没有传递到其他线程中去,这就会导致数据更新的不及时。

        - 关键步骤缺乏加锁限制:关键步骤就是对数据进行更新的步骤,在上述例子中,tickets--就是一个关键步骤,当对同一个变量做修改操作的时候,我们需要对其进行加锁限制。

数据不一致问题解决

         针对变量副本带来的问题,我们采用 volatile 关键字来修饰变量,从而保证不同线程对共享变量操作时的可见性。

public class ThreadDemo2 {

    public static void main(String[] args) throws Exception {
        TestThread2 t = new TestThread2();
        t.start();
        Thread.sleep(2000);
        t.flag = false;
        System.out.println("main thread is exiting");
    }
}

class TestThread2 extends Thread {

    boolean flag = true; //子线程不会停止
    //volatile boolean flag = true; //用volatile 修饰的变量可以及时的在各线程里面通知

    public void run() {
        for(int i = 0; this.flag; ++i) {}

        System.out.println("test thread3 is exiting");
    }
}

         那对于关键步骤缺乏加锁限制呢,我们可以编写代码实现线程互斥,也可以使用关键字 synchronized 来进行加锁,被它修饰的代码块/函数,一次只能允许一个线程进入,虽然使用简便,但是也加大了性能负担!

public class ThreadDemo3 {

    public static void main(String[] args) {
        TestThread3 t = new TestThread3();
        (new Thread(t, "Thread-0")).start();
        (new Thread(t, "Thread-1")).start();
        (new Thread(t, "Thread-2")).start();
        (new Thread(t, "Thread-3")).start();
    }
}

class TestThread3 implements Runnable {
    private volatile int tickets = 100;  //多个线程共享的
    String str = new String("");

    public void run() {
        do {
            this.sale();

            try {
                Thread.sleep(100);
            } catch (Exception var2) {
                System.out.println(var2.getMessage());
            }
        } while(this.tickets > 0);
    }

    //同步代码块,一次只能一个线程进来
    public synchronized void sale() {
        if (this.tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " is saling ticket " + this.tickets--);
        }
    }
}

多线程管理

线程状态及其关系

        要对多线程进行管理,还得知道线程有哪些状态。首先,线程的五个状态如下:(有的地方也会谈到七状态,包含了就绪挂起状态和阻塞挂起状态)

        - NEW   刚创建(new)

        - RUNNABLE   就绪态 (start)

        - RUNNING    运行中(run)

        - BLOCK   阻塞(sleep)

        - TERMINATED  结束

        它们之间的转换关系如图所示,注意,就绪是所有的资源都已备好,只等调度;等待(阻塞)是还在等待某项资源! 

 阻塞/唤醒的API

        线程的创建、运行和终止的代码我们在前面已经讲述过,现在该开始学习线程的阻塞和唤醒的API~,早在前面我们也已经见过一个sleep函数了。

        - sleep,线程自我休眠,时间一到,自己就会醒来(阻塞态和就绪态之间的转换)

        - wai / notify / notifyAll,被动等待,需要别人来进行唤醒

        - join,等待另一个线程结束

        - interrupt,向另一个线程发送中断信号,该线程收到信号,会触发InterruptedException(可解除阻塞),并进行下一步处理。

        这里需要注意的是,线程被动的暂停和终止,是依靠别的线程来拯救自己,并不会及时的释放资源,为了避免资源得不到立即释放,所以我们一个让线程主动暂停和终止!

具体的我们可以:

  1. 定期监测共享变量,如设置一个flag;
  2. 如果需要暂停或终止,先释放资源,再主动动作

【暂停:Thread.sleep(),休眠;终止:run方法结束,线程终止】

多线程死锁

        多线程的死锁是指每个线程互相持有别人需要的锁(如哲学家吃面),这里简单写一个例子如下:

public class ThreadDemo5 {
    public static Integer r1 = 1;
    public static Integer r2 = 2;

    public static void main(String[] args) throws InterruptedException {
        TestThread51 t1 = new TestThread51();
        t1.start();
        TestThread52 t2 = new TestThread52();
        t2.start();
    }
}

class TestThread51 extends Thread {

    public void run() {
        //索要r1
        synchronized(ThreadDemo5.r1) {
            try {
                TimeUnit.SECONDS.sleep(3);
                //TimeUnit 是JDK 5引入的新类,提供了时间单位粒度和一些时间转换、计时和延迟等函数
            } catch (InterruptedException var4) {
                var4.printStackTrace();
            }
            //索要r2
            synchronized(ThreadDemo5.r2) {
                System.out.println("TestThread51 is running");
            }
        }
    }
}

class TestThread52 extends Thread {

    public void run() {
        //索要r2
        synchronized(ThreadDemo5.r2) {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException var4) {
                var4.printStackTrace();
            }
            //索要r1
            synchronized(ThreadDemo5.r1) {
                System.out.println("TestThread52 is running");
            }
        }
    }
}

        可以看出它们彼此都拿到了对方的资源,从而导致线程阻塞。要想预防死锁,我们可以对资源进行等级排序 ,即把例子中的 TestThread52 也统一规定为先拿 r1,再拿 r2 。

守护(后台)线程

        通过前面的学习,我们已经知道普通线程的结束,是run方法运行结束,而守护线程的结束,是run方法运行结束,或者main函数结束。你可以理解为守护线程的run方法要么先于main函数结束,要么main函数结束,整个程序结束。还是通过一个例子来加以说明:

public class ThreadDemo4 {

    public static void main(String[] args) throws InterruptedException {
        TestThread4 t = new TestThread4();
        t.setDaemon(true);   //定义守护线程(后台线程)
        t.start();
        Thread.sleep(2000);
        System.out.println("main thread is exiting");
    }
}

class TestThread4 extends Thread {

    public void run() {
        while(true) {
            System.out.println("TestThread4 is running");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException var2) {
                var2.printStackTrace();
            }
        }
    }
}

        注意:守护线程永远不要访问资源,如文件或者数据库;因为main函数结束的时候,它可能来不及释放资源!

        接下来是高并发部分,篇幅原因,咱们明天再来打卡!

        

        

  • 12
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值