《疯狂java讲义》读书笔记(七):多线程

《疯狂java讲义》读书笔记(七):多线程

1.线程和进程

​ 这两个概念必须得区分,之前学操作系统的时候我老给弄混了。
​ 进程的三种基本状态:

(1)运行态(running)
​ ​ ​ ​ ​ ​ ​ 当进程得到处理机,其执行程序正在处理机上运行时的状态称为运行状态。
​ ​ ​ ​ ​ ​ ​ 在单CPU系统中,任何时刻最多只有一个进程处于运行状态。在多CPU系统中,处于运行状态的进程数最多为处理机的数目。
(2)就绪状态(ready)
​​ ​ ​ ​ ​ ​ ​ 当一个进程已经准备就绪,一旦得到CPU,就可立即运行,这时进程所处的状态称为就绪状态。系统中有一个就绪进程队列,处于就绪状态进程按某种调度策略存在于该队列中。
(3)等待态(阻塞态)(Wait / Blocked )
​ ​ ​ ​ ​ ​ ​ 若一个进程正等待着某一事件发生(如等待输入输出操作的完成)而暂时停止执行的状态称为等待状态。 处于等待状态的进程不具备运行的条件,即使给它CPU,也无法执行。系统中有几个等待进程队列(按等待的事件组成相应的等待队列)。

​ 进程和线程的区别:
《疯狂java讲义》读书笔记(七):多线程_第1张图片
​​ ​ ​ ​ ​ ​ ​ 使用多线程编程的好处:进程之间不能共享内存,但是线程贡献内存就很方便系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小很多Java语言内置了多线程功能支持,而不是单纯的作为底层操作系统的调度方式,从而简化了Java多线程编程

2.线程的创建和启动

​​ ​ ​ ​ ​ ​ ​ 这部分内容说实话还挺熟悉的,还记得当时考Java,在b站看多线程的视频,当时期末考试还重点考了多线程hhhh。三种方式不多说。

2.1继承Thread类创建线程类

​ 直接写代码了。

//1.定义Thread子类
public class FirstThread extends Thread {
	private int i;
	//2.重写run方法,该run方法代表了线程需要完成的任务
	@Override
	public void run() {
		for (;i<100;i++){
			/**
			 * Thread对象的getName()返回当前线程的名字
			 * 当前线程类继承Thread,直接使用this就能获取当前线程
			 */
			System.out.println(getName()+" "+i);
		}
	}

	public static void main(String[] args) {
		for (int i=0;i<100;i++){
			System.out.println(Thread.currentThread().getName()+""+i);
			if (i==20){
				//3.创建Thread子类的实例,调用start方法启动线程
				new FirstThread().start();
				new FirstThread().start();
			}
			
		}
	}
}
2.2实现Runnable接口

​ 还是直接写代码了,用一个售票员卖票的例子说明。

public class Seller implements Runnable{
    private int ticket;
    public Seller(int ticket){
        this.ticket=ticket;
    }
    @Override
    public void run(){
        while (ticket>0){
            ticket--;
            System.out.println("剩余"+ticket+"张票");
        }
    }
}

主类。

public class Main {
    public static void main(String[] args) {
        System.out.println("开始卖票!");
        new Thread(new Seller(100)).start();
        new Thread(new Seller(20)).start();
    }
}
2.3使用Callable和Future
public class ThreadTest {

    public static void main(String[] args) {

        Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask对象作为Thread对象的target创建新的线程
                thread.start();                      //线程进入到就绪状态
            }
        }

        System.out.println("主线程for循环执行完毕..");
        
        try {
            int sum = ft.get();            //取得新创建的新线程中的call()方法返回的结果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}


class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}

3.线程的生命周期

​ ​ ​ ​ ​ ​ ​ 新建、就绪、运行、阻塞、死亡。

​ ​ ​ ​ ​ ​ ​ new关键字创建了一个线程之后,线程处于新建状态,此使它和其它对象一样,仅仅由JVM为其分为内存,并初始化其成员变量的值,此时的线程对象没有表现成任何线程的动态特征,程序也不会执行线程的线程执行体。当调用start()方法之后,线程处于就绪状态,JVM会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了,至于啥时候开始,还是要取决于JVM里线程调度器的调度。

​ ​ ​ ​ ​ ​ ​ 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当一个线程开始运行后,不可能总运行,线程在运行过程会被终端,目的是使其他线程获得执行的机会。当发生下列情况的时候,线程会进入阻塞状态。

  • 调用sleep()方法主动放弃所占用的处理器资源。

  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。

  • 线程试图获得一个同步监视器,但该同步监视器整备其他线程所持有。

  • 线程在等待某个通知(notify)。

  • 程序调用了线程的suspend()方法将线程挂起,该方法容易造成死锁

    被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态不是运行态。

​ ​ ​ ​ ​ ​ ​ 线程会以三种方式结束,结束后处于死亡态。run()或者call()方法执行完成,线程正常结束。线程抛出一个没有捕获的Exception或者Error。直接调用该线程的stop()方法结束

​ ​ ​ ​ ​ ​ ​ 别对死亡状态的线程调用start()方法,程序只能对新建状态的线程使用start()方法,对一个新建状态的线程两次调用start()方法也是错误的。

4.控制线程

4.1 join线程

​​ ​ ​ ​ ​ ​ ​ 让一个线程等待另一个线程完成的方法。当在某个程序执行流中调用其它线程的join方法时,调用线程将被阻塞,一直到被join()方法加入的join线程执行完为止。

4.2 后台线程

​ ​ ​ ​ ​ ​ ​ JVM的垃圾回收线程就是典型的后台线程。后台线程有个特点就是如果所有前台线程都死亡,后台线程就会自动死亡。调用Thread对象的setDaemon(true)方法可指定线程设置为后台线程。

4.3线程睡眠:sleep

​ ​ ​ ​ ​ ​ ​ 让正在执行的线程暂停一会,进入阻塞状态。

4.4线程让步:yield

​ ​ ​ ​ ​ ​ ​ 和sleep()差不多,不过它是让正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转入就绪状态。意思就是说,该方法只是让当前线程暂停下,让系统的线程调度器重新调度一次。

sleep()和yield()的区别

  • sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级,但yield()只会给优先级相同或者更高的线程执行机会。
  • sleep()比yield()有更好的可移植性
  • sleep()会将线程转入阻塞态,直到经过阻塞时间才会转入就绪状态,而yield()不会将线程转入阻塞态
  • sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常,而yield()没有声明抛出任何异常。

5.线程同步

5.1同步代码块

​​ ​ ​ ​ ​ ​ ​ 使用synchronized将某方法里的方法体修改成同步代码块。保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区,所以同一时刻最多只有一个线程处于临界区内,从而保证线程安全。

5.2同步方法

​​ ​ ​ ​ ​ ​ ​ 就是用synchronized关键字就是某个方法。该类的对象可以被多个线程安全访问,每个线程调用该对象的任意方法之后都将得到正确结果,每个线程调用该对象的任意方法之后,该对象依然保持合理状态。

public synchronized void draw(double drawAmount){
  ...
}

synchronized可以修饰方法和代码块,但是构造器和成员变量不行。

6.线程通信

​ ​ ​ ​ ​ ​ ​ 传统的线程通信依赖于Object类中的wait()、notify()、notifyAll()三个方法,这三个方法不属于Thread类!!使用synchronized修饰的同步方法可以在方法内直接调用下列三个方法,因为默认实例就是同步监视器,但是如果是同步代码块,同步监视器就是synchronized后括号里的对象,必须使用该对象调用三个方法。

​​ ​ ​ ​ ​ ​ ​ wait():导致当前线程等待,直到其它线程调用该同步监视器的notify()或者notifyAll()方法来唤醒该线程。调用该方法的当前线程会释放对该同步监视器的锁定

​ ​ ​ ​ ​ ​ ​ notify():唤醒在此同步监视器上等待的单个线程。

​ ​ ​ ​ ​ ​ ​ notifyAll():唤醒再此同步监视器上等待的所有线程。

​ ​ ​ ​ ​ ​ ​ 当使用Lock对象保证同步的话,就不能用上面三个方法了,可以用Condition类来保持协调。对应的方法是await()、signal()、signalAll()

7.线程相关类

7.1 ThreadLocal类

​ ​ ​ ​ ​ ​ ​ 该类只提供了三个public方法。ThreadLocal将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而没有必要对该变量进行同步了。该类不同替代同步机制,面向的问题领域不同。ThreadLocal是为了隔离多个线程的数据贡献,从根本上避免多个线程之间对共享资源的竞争,而同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式。

T get():返回此线程局部遍历中当前线程副本中的值。
void remove():删除此线程局部变量中当前线程的值。
void set(T value):设置此线程局部变量中当前线程副本的值。
7.2包装线程不安全的集合

​ ​ ​ ​ ​ ​ ​ 之前的读书笔记中提到过一些线程不安全的类,在那一节笔记中,也提到了Collections对这些类进行包装,使之成为线程安全的类。

7.3线程安全的集合类

​ ​ ​ ​ ​ ​ ​ 还记得有个包叫做java.util.concurrent嘛,它就提供了大量支持高并发访问的集合接口和实现类。不过这些集合类主要分为以Concurrent开头的集合类还有CopyOnWrite开头的集合类,在这里不多阐述了。

你可能感兴趣的:(java笔记)