JavaSE--多线程

1.并发与并行

●并发:指两个或多个事件在同一时间段内发生。(交替执行)
●并行:指两个或多个事件在同一时刻发生。(同时发生)
即,并发就是你洗完澡后再听歌,并行就是你一边洗澡一边听歌。

2.进程与线程

(线程<进程)
●进程:程序的执行过程。(可在任务管理器查看)
●线程:进程中的一个执行单元。
一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

举例:Word的使用。
	每次打开一个Word就相当于启动了一个进程,
	在这个进程上又有许多其他程序在执行(例如:拼写检查,自动更正等),这些就是一个个线程。
	如果Word关闭了,这些线程就会全部消失。但是如果这些线程消失了,Word不一定会消失。

线程一定得依附于进程才能够存在。
“同时”执行是线程给人的感觉,在线程之间实际上是轮换执行。

3.线程调度

(1)分时调度:
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
(2)抢占式调度:
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。java使用的为抢占式调度。
●设置线程的优先级:
打开任务管理器>选择希望设置优先级的进程>右键>转到详细信息>设置优先级

4.多线程的实现:

在Java中要想实现多线程的程序,必须依靠一个线程的主体类,即主线程(执行主方法(main)的线程)。然后此类继承Thread类或实现Runnable接口。
在这里插入图片描述

1)继承Thread类

java.lang.Thread是操作线程的类,任何类只需要继承Thread类就可以成为一个线程的主类。

Thread类下的两个重要方法:
run()start()方法。
线程执行体:run()。(线程需要完成的任务)
线程的起点:start()。(线程的启动,启动后执行的方法体是run()方法定义的代码)
程序的起点:main()。

//1.创建一个Thread类的子类
public class MyThread extends Thread{
    //2.重写Thread类中的run方法,设置线程任务
    @Override
    public void run(){
        for (int i = 0; i < 20; i++) {
            System.out.println("run:"+i);
        }
    }
}
public class doMain {
    public static void main(String[] args) {
        //3.创建Thread类的子类对象
        MyThread mt = new MyThread();
        //4.调用Thread类中的start(),开启新的线程,执行run()
        mt.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("main:"+i);
        }
    }
}

随机性打印结果的原因:
在这里插入图片描述
多线程内存图解:
在这里插入图片描述
Thread类的常用方法:

getName() 取得线程名字
setName() 设置线程名字
currentThread() 取得线程名字
sleep(long millitime) 使当前正在执行的程序以指定的毫秒数暂定

Thread.currentThread().getName() 和 this.getName()的区别

2)实现Runnable接口:

为了避免单继承局限的问题,我们可以使用Runnable接口来实现多线程。
要启动多线程,就一定需要通过Thread类中的start()方法,但是Runnable接口中没有提供可以被继承的start()方法。这时就需要借住Thread类中提供的有参构造方法:

public Thread(Runnable target) 此方法可以接收一个Runnable接口对象
//MyThread是实现了Runnable接口的子类
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
new Thread(mt).start();
new Thread(mt2).start();

3)实现Runnable接口的好处:

①避免单继承局限;
②降低程序的耦合性,方便解耦。即把设置线程任务(实现类中重写run())和开启新线程(Thread类对象调用start())进行了分离。

4)多线程的两种实现方式及区别:

●它们的实现都需要一个线程的主类,都必须在子类中覆写run()方法,都必须调用Thread类中的start()方法来开启线程。

Thread类是Runnable接口的子类,而使用Runnable接口可以避免单继承局限,方便解耦,并且可以更加方便地实现数据共享的概念。
public class Thread extends Object implements Runnable

●它们的结构:

Runnable接口 Thread类
class MyThread implements Runnable{} class MyThread extends Thread{}
new Thread(mt).start(); mt.start();

5)使用匿名内部类实现多线程的创建:

ublic class doMain {
    public static void main(String[] args) {
        //1.线程的父类是Thread
        new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+">--"+"a");
                }
            }
        }.start();

        //2.线程的接口Runnable
        new Thread(new Runnable(){
                @Override
                public void run() {
                    for (int i = 0; i < 20; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"b");
                    }
                }
            }).start();
    }
}

5.线程的操作状态:

要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有以下五种状态:
在这里插入图片描述

  1. 创建状态
    新线程对象处于新建状态时
  2. 就绪状态
    新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。
  3. 运行状态
    当就绪的线程被调度并获得CPU资源时,便进入运行状态。此时,自动调用该线程对象的run()方法,run()方法定义了线程的操作和功能
  4. 堵塞状态
    在某种特殊情况下,被人为挂起或执行输入输出操作时,将让出 CPU 并临时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以进入就绪状态。
  5. 终止状态
    线程调用stop()或run()方法执行结束后,就处于终止状态。处于终止状态的线程不具有继续运行的功能。

6.线程的休眠与优先级:

1)线程的休眠:
是让程序执行速度变慢一些。在Thread类中线程休眠操作方法为:
public static void sleep(long millis) throws InterruptedException
设置的休眠单位是毫秒(ms)。

2)线程的优先级:
对高优先级,使用优先调度的抢占式策略。哪个线程的优先级高,哪个线程就有可能被执行。

  1. MAX_PRIORITY : 10
  2. MIN _PRIORITY:1
  3. NORM_PRIORITY:5

线程优先级操作方法:

  1. setPriority(int p) :设置线程的优先级
  2. getPriority() :取得线程优先级

7.线程安全问题(同步与死锁):

先来解释不同步遇到的问题:
如果分成三个窗口卖100张票,假如不同步的话,就有可能出现三个窗口卖重票、错票的情况。(多个线程操作同一资源可能出现的情况,因为前面的线程还没完成操作,其它线程也进来操作车票。(抢占))

实现三个窗口来卖票的程序:

//实现卖票程序
public class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //设置线程任务:卖票
    @Override
    public void run() {
        //先判断票是否存在
        while(true){
        if (ticket>0) {
            //票存在,卖票
            System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
            ticket--;
            }
        }else {
            break;
        }
    }
}
	
public class doMain {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}

解决方法:通过同步操作来解决。

  1. 同步操作:一个代码块中的多个操作在同一时间段内只能由一个线程进行,其他线程要等待此线程完成后才可以继续执行。
    在这里插入图片描述

以下有三种方式完成同步操作:
其实就是添加上图中的“锁”。

1)同步代码块:

synchronized(this){
      //需要被同步操作的代码
}

关于this的解释:

  1. 在实现Runnable接口创建多线程的方式中,我们可以使用this充当所,代替手动new一个对象,因为后面我们只创建一个线程的对象。
  2. 在继承Thread类创建多线程的方式中,慎用this,考虑我们的this是不是唯一的。我们可以使用当前类来充当这个是锁。synchronized (类名.class)

使用同步代码块完成同步操作:
主要有变化的在run()方法里,doMain类不变。

//实现卖票程序
public class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //设置线程任务:卖票
    @Override
    public void run() {
        while(true){
        //先判断票是否存在
            synchronized (this){
                if (ticket>0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //票存在,卖票
                    System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }
}

2)同步方法:

利用synchronized定义的方法。

//实现卖票程序
public class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //设置线程任务:卖票
    @Override
    public void run() {
        while(true){
            sale();
        }
    }
    public synchronized void sale(){
        //先判断票是否存在
        synchronized (this){
            if (ticket>0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票
                System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

补充:

  1. 非静态同步方法,锁对象:this
  2. 静态同步方法,锁对象:RunnableImpl.class

3)Lock锁:

java.util.concurrent.locks.Lock接口
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。

Lock接口中的方法:

void Lock() 获取锁
void unlock() 释放锁

Lock接口的实现类:ReentrantLock

java.util.concurrent.locks.ReentrantLock implements Lock

使用Lock锁完成同步操作:
三步走。1.创建ReentrantLock对象 2.获取锁 3.释放锁

//实现卖票程序
public class RunnableImpl implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //1.创建ReentrantLock对象
    Lock lk = new ReentrantLock();
    //设置线程任务:卖票
    @Override
    public void run() {
        while(true){
            //2.在可能出现线程安全的代码前调用Lock接口中的Lock方法获取锁.
            lk.lock();
            if (ticket>0) {
                try {
                    Thread.sleep(10);
                    //票存在,卖票
                    System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能出现线程安全的代码后释放锁。
                    lk.unlock();
                }
            }
        }
    }
}

4)比较 synchronized 与 Lock:

  1. synchronized是自动释放锁(显示),lock需要手动释放和关闭锁(隐式)。
  2. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  3. Lock只有代码块锁,synchronized有代码块锁和方法锁

5)总结:

加入同步后明显比不加入同步慢许多,所以同步的代码性能低,但是数据安全性高。

6)常见面试题分析:

  1. 同步和异步有什么区别。什么情况下使用?
    如果一块数据要在多个线程间共享,则必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,那么就应该用异步编程,在很多情况下采用异步途径往往更有效率。

  2. abstract的method是否可以同时是static,是否可以同时是native、synchronized?
    method、static、native、synchronized都不能和“abstract”同时声明方法。

  3. 当一个线程进入一个对象的synchronized方法后,其他线程是否可访问此对象的其他方法?
    不能访问,一个对象操作一个synchronized方法只能由一个线程访问。(其他线程等待)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章