学习Java多线程这一篇就够了

目录

进程与线程

进程基本概念及理解

线程基本概念及理解

进程线程对比

创建进程

继承Thread类创建多线程

实现Runnable接口创建多线程

实现Callable接口创建多线程

Thread类与Runnable接口实现多线程对比

线程状态

线程调度

线程的优先级

线程休眠

线程插队

线程让步

线程中断

守护线程

线程同步

同步代码块

同步代码块

同步方法

死锁问题

Lock锁


进程与线程

多线程是提升程序性能重要方式,也是java中一项重要的技术。多线程是指一个应用程序中有多条并发执行的线索,每条线索都是一个线程,线程之间可以可以交替执行,彼此可以通信。

进程基本概念及理解

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

进程则是一个动态的概念,进程则是执行程序的一次执行过程,是系统进行资源分配和调度的基本单位。

  • 程序就是保存在磁盘上,编译好的二进制文件,不占用系统资源
  • 进程存在于内存中,占用系统资源
  • 一个单核的CPU同一时刻只能处理一个进程

在单核CPU背景下,平时我们在一台计算机上,可以同时打游戏,听音乐,语音通话可以同时进行,感觉是有多个进程在运行,但实际上是计算机系统采用了多到程序设计技术,就是在内存管控下,多个进程之间相互穿插执行。

采用多到程序设计的系统会把CPU周期划分为多个长度相同的时间片,在每个时间片内只能处理一个进程,也就是说内存里面的多个进程轮流交替使用CPU。而我们产生错觉的原因是,CPU的时间片非常小,我们难以感受到其中差异。因此在宏观上,可以认为是计算机可以并发执行多个进程。

线程基本概念及理解

每个运行的程序就是以一个进程,一个进程里面可以有多个执行单元同时执行,这些执行单元就是线程,每个进程至少有一个线程。线程是CPU调度和执行的的单位。

多线程和单线程的区别:

多线程看起是同时执行的,他们和进程是一样的,由CPU轮流执行,只是CPU执行速度很快,几乎感觉不到。

真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉

进程线程对比

  1. 进程是系统进行资源分配和调度的基本单位,线程是CPU调度和执行的的单位。
  2. 一个进程里面至少包含一个多线程,可以包含多个。

关于多线程理解:

  1. 线程就是独立的执行路径;
  2. 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
  3. main() 称之为主线程,为系统的入口,用于执行整个程序;
  4. 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
  5. 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  6. 线程会带来额外的开销,如cpu调度时间,并发控制开销
  7. 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

创建进程

三种创建进程方式:

继承Thread类创建多线程

通过继承Thread类,重写类中run方法,即可实现多线程。

start() 启动新线程,之后Java虚拟机自动调用run方法。

package Example1;

// 继承Thread类创建多线程

public class Test1 {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();
        while(true) {
            System.out.println("main()方法在运行");
        }

    }
}

class Mythread extends Thread {
    public void run() {
        while(true) {
            System.out.println("MyThread类的run方法在运行");
        }
    }
}

运行结果:

Mythread.start()启动新的线程,和之前main()线程里的代码同时执行。

实现Runnable接口创建多线程

Thread类提供了一个构造方法,Thread(Runnable target) ,参数是一个接口,它只有一个run方法。

通过这个构造方法创造Thread对象时,需要传递一个Runnable实现接口对象。通过Thread的对象调用start()方法,来执行run()。

这样一来,调用新线程任务就交给Thread执行,原本的类只需要实现Runnable接口即可。可以避免Java单继承的问题。

package Example2;

public class Test2 {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        Thread thread = new Thread(mythread);
        thread.start();
        while(true) {
            System.out.println("main()方法在运行");
        }
    }
}

// Mythread实现Runnable()接口,并且重写run()方法
class Mythread implements Runnable {
    public void run() {
        while(true) {
            System.out.println("MyThread类的run方法在运行");
        }
    }
}

运行结果:

实现Callable接口创建多线程

前两种创建进程方法存在缺点,无法从run()方法获取返回值。

实现Callable接口可以满足创建多线程和获取返回值的要求。

package Example3;
import java.util.concurrent.*;

// 实现Callable创建多线程
public class Test3 {
    public static void main(String[] args) throws InterruptedException,ExecutionException{
        // 创建实现Callable接口的实例对象
        Mythread mythread = new Mythread();
        // 使用FutureTask封装Mythread类
        FutureTask<Object> ft = new FutureTask<>(mythread);
        // 使用Thread(Runnable target,String name)创建线程对象
        Thread thread = new Thread(ft,"thread");
        // 启动新线程
        thread.start();
        // currentThread().getName() 当前线程名字  get() 返回管理值
        System.out.println(Thread.currentThread().getName()+"的返回结果"+ft.get());
        int a=0;
        while(a++<5) {
            System.out.println("main()方法在运行");
        }
    }
}

// 实现Callable接口
class Mythread implements Callable<Object> {
    public Object call() throws Exception {
        int i = 0;
        while (i++ < 5) {
            System.out.println(Thread.currentThread().getName() + "的call()方法在运行");
        }
        return i;
    }
}

Thread类与Runnable接口实现多线程对比

1. Thread类实现多线程时通过结成Thread的方式实现的,而Runnable接口实现的方式可以解决Java单继承的问题。

2. Thread类实现多线程无法资源共享,而Runnable接口实现却可以做到

package Example4;

public class Test4 {
    public static void main(String[] args) {
        new TicketWindows().start(); // Thread-0
        new TicketWindows().start(); // Thread-1
        new TicketWindows().start(); // Thread-2
        new TicketWindows().start(); // Thread-3
    }
}

class TicketWindows extends Thread {
    private int tickets = 100;
    public void run() {
        while(tickets>0) {
            // 调用Thread类种的currentThread().get()获取当前执行线程的名字
            Thread th = Thread.currentThread(); //返回一个对象
            String name = th.getName();
            System.out.println(name+"正在发售第" + tickets-- + "张票");
        }
    }
}
package Example5;

public class Test5 {
    public static void main(String[] args) {
        TicketWindow tk = new TicketWindow();
        new Thread(tk,"窗口1").start();
        new Thread(tk,"窗口2").start();
        new Thread(tk,"窗口3").start();
        new Thread(tk,"窗口4").start();
    }
}

class TicketWindow implements Runnable {
    private int tickets=100;
    public void run () {
        while(tickets>0) {
            Thread th =Thread.currentThread();
            String name = th.getName();
            System.out.println(name + "正在发售第" + tickets-- + "张票");
        }
    }

}

综合考虑,建议在创建多线陈的时候使用实现Runnable方式创建多线程。

线程状态

线程的基本状态:新建,运行,阻塞,等待,中止

线程调度

线程调度有两种模型:分时调度模型和抢占式调度模型

  • 分时调度模型:所有线程轮流获得CPU的使用权,就是平均主义
  • 抢占式调度模型:根据线程优先级获得CPU使用权,优先级相同则线程随机获得CPU使用权

Java虚拟机默认采用抢占式调度模型

线程的优先级

优先级高的的线程获得CPU使用权的机会就越大,优先级低的线程获得CPU使用机会越小。

线程的优先级用1-10,数字越大,优先级越高。Thread类中提供了三个静态常量表示线程的优先级。

static int MAX_PRIORITY:表示线程最高优先级,表示10;

static int MIN_PRIORITY :表示线程最低级,表示1;

static int NORM_PRIORITY:表示线程默认优先级,表示5;

package Example6;

public class Test6 {
    public static void main(String[] args) {
        Thread mp = new Thread(new MaxPriority(),"优先级较高线程");
        Thread np = new Thread(new MinPriority(),"优先级较低线程");
        mp.setPriority(Thread.MAX_PRIORITY);
        np.setPriority(Thread.MIN_PRIORITY);
        mp.start();
        np.start();
    }
}

class MaxPriority implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"正在输出"+i);
        }
    }
}

class MinPriority implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"正在输出"+i);
        }
    }
}

其实,即使设置了线程的优先级,一样无法确保这个线程一定先执行,因为它有很大的随机性。它并无法控制执行哪个线程,因为线程的执行,是抢占资源后才能执行的操作,而抢点资源时,最多是给于线程优先级较高的线程一点机会而已,能不能抓住可是不一定的。。

线程休眠

是指让当前线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其它线程的调度方式

package Example7;

public class Test7 {
    public static void main(String[] args) throws Exception {
        new Thread(new SleepThread()).start();
        for(int i=1;i<=8;i++){
            if(i==5){
                Thread.sleep(2000); // 这是一个静态方法,只能控制当前的进程,不能控制其它进程
            }
            System.out.println("主线程正在输出:"+i);
            Thread.sleep(500);
        }

    }
}

class SleepThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 8; i++) {
            if(i==3) {
                try{
                    Thread.sleep(2000);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("SleepThread线程正在输出:"+i);
            try{
                Thread.sleep(500);
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
这是一个静态方法,只能控制当前的进程,不能控制其它进程

线程插队

线程插队指暂停当前线程,转而之心跟另外一个线程,原来线程进入阻塞状态,直到但钱线程执行完毕,在执行原来的线程。

package Example8;

public class Test8 {
    public static void main(String[] args) throws InterruptedException{
        Thread th = new Thread(new JoinRunnable(),"thread");
        th.start();
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
            if(i==2) {
                th.join();
            }
        }

    }
}

class JoinRunnable implements Runnable {
    @Override
    public void run() {
        for(int i=1;i<=3;i++){
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
        }
    }
}

Thread还提供了带有时间参数的线程插队方法,必须等待插入时间过后,其它线程才能执行,而且时间过后,其它线程都可以与当前线程争夺CPU资源。

package Example9;

public class Test9 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new JoinRunnable(),"thread");
        thread.start();
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
            if(i==2){
                thread.join(3000);
            }
        }
    }
}

class JoinRunnable implements Runnable {
    @Override
    public void run() {
        for(int i=1;i<=3;i++){
            try{
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
        }
    }
}

线程让步

线程让步是指在某个时间点,当前线程从就绪状态或执行状态转到阻塞状态,将CPU资源让给其它线程使用。

package Example10;

public class Test10 {
    public static void main(String[] args) {
        Thread t1 = new YieldThread("t1");
        Thread t2 = new YieldThread("t2");
        t1.start();
        t2.start();
    }
}

class YieldThread extends Thread {
    public YieldThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
            if(i==2){
                System.out.println("线程让步:");
                Thread.yield();
            }
        }
    }
}

 yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

线程中断

是指在线程执行过程中通过手动操作停止线程。例如:用户执行一次操作,因为网络问题导致延迟,这个线程就一直处于运行状态。如果用户幼结束这个操作,就需要线程中断机制。

interrupt():中断当前进程

isInterrupt():判断当前进程状态 ture-已中断 false-未中断

package Example11;

public class Test11 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if(i==5){
                        Thread.currentThread().interrupt();
                        System.out.println("thread线程是否已中断---"+Thread.currentThread().isInterrupted());
                    }
                }
            }
        });
        thread.start();
    }
}

守护线程

  1. 线程分为用户线程和守护线程
  2. 虚拟机必须确保用户线程执行完毕,比如:main();
  3. 虚拟机不用等待守护线程执行完毕
package Example12;

public class Test12 {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();
        Thread thread = new Thread(god);
        thread.setDaemon(true); // 设置守护进程,true为守护线程,false默认用户线程
        thread.start();
        new Thread(you).start();
    }

}

class God implements Runnable {
    @Override
    public void run() {
        while(true) {
            System.out.println("上帝保佑你");
        }
    }
}


class You implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("你还活着");
        }
        System.out.println("Goodbye World!");
    }
}

用户进程结束后,守护进程就会结束。

线程同步

多线程的并发执行可以提高程序的效率。多个线程共享资源时,会引发安全问题。

同步代码块

线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决这个问题,就必须保证在任何时刻都只能有一个线程访问共享资源。

Java中提供了同步机制,当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块,这个代码块被称为同步代码块。

当某个线程执行当前同步代码块时,其它线程无法执行同步代码块,进入阻塞状态。当前线程执行完同步代码块后,再与其它线程重新抢夺CPU的执行权。

同步代码块

package Example14;

public class Test14 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket,"线程1").start();
        new Thread(ticket,"线程2").start();
    }
}

class Ticket implements Runnable {
    private int tickets=100;
    Object lock = new Object(); // 锁对象的创建不能放在run()方法里面,它是锁的唯一标志

    @Override
    public void run() {
        while(true) {
            synchronized (lock) {
                try{
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(tickets>0) {
                    System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
                }else {
                    break;
                }
            }
        }
    }
}

同步方法

synchronized还可以修饰方法,同步方法和同步代码块作用一样。

package Example15;

public class Test15 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket,"线程1").start();
        new Thread(ticket,"线程2").start();
    }
}

class Ticket implements Runnable {
    private int tickets=100;

    @Override
    public void run() {
        while(true) {
            saleTicket();
            if(tickets<=0) {
                break;
            }
        }
    }

    private synchronized void saleTicket(){
        if(tickets>0) {
            try{
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
        }
    }
}

同步代码块的锁是自己定义的锁对象。同步方法的锁是this,即当前调用方法的线程。确保了锁的唯一性。

在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题

死锁问题

多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而 导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 . 某一个同步块 同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 死锁问题。

产生死锁问题的原因:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。

只要能破解其中任何一个条件,就能解开死锁。

Lock锁

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对 象来实现同步。同步锁使用Lock对象充当

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开 始访问共享资源之前应先获得Lock对象

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语 义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

package Example16;

import java.util.concurrent.locks.ReentrantLock;

public class Test16 {
    public static void main(String[] args) {
        TestLock tk = new TestLock();
        new Thread(tk).start();
        new Thread(tk).start();
        new Thread(tk).start();
        new Thread(tk).start();
    }
}

class TestLock implements Runnable {
    int tickets = 10;
    // 定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true) {
            try{
                lock.lock();// 加锁
                if(tickets>0){
                    try {
                        Thread.sleep(1000);
                    } catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(tickets--);
                }else {
                    break;
                }
            } finally {
                lock.unlock(); // 解锁
            }
        }
    }
}

synchronized与lock对比:

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了 作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展 性(提供更多的子类)

到此多线程的学习先告一段落了,还差一个经典的问题:生产者消费者,需要解决线程通信的问题,后续会补上的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dao-道法自然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值