多线程

1 什么是线程, 线程与进程的关系

线程是程序的执行路径,或者可以说是程序的控制单元。
一个进程可能包含一个或多个线程,当一个进程存在多条执行路径时,就可以将该执行方式称为多线程。
线程的执行方式大致可分为就绪 (wait),执行(run),阻塞(block) 三个状态,而三个状态的转换实质上是在抢夺 cpu 资源过程中造成的,正常情况下 cpu 资源不会被线程独自占用,因此多个线程在运行中相互抢夺资源,造成线程在上述的三个状态之间不断的相互转换。而这也是多线程的执行方式。

2 同步和异步的区别

同步(Sync)
所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
根据这个定义,Java 中所有方法都是同步调用,应为必须要等到结果后才会继续执行。我们在说同步、异步的时候,一般而言是特指那些需要其他端协作或者需要一定时间完成的任务。
简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。
异步(Async)
异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

举个例子简单说明下两者的区别:
同步:火车站多个窗口卖火车票,假设 A 窗口当卖第 288 张时,在这个短暂的过程中,其他窗口都不能卖这张票,也不能继续往下卖,必须这张票处理完其他窗口才能继续卖票。直白点说就是当你看见程序里出现 synchronized 这个关键字,将任务锁起来,当某个线程进来时,不能让其他线程继续进来,那就代表是同步了。

异步:当我们用手机下载某个视频时,我们大多数人都不会一直等着这个视频下载完,而是在下载的过程看看手机里的其他东西,比如用 qq 或者是微信聊聊天,这种的就是异步,你执行你的,我执行我的,互不干扰。比如上面卖火车票,如果多个窗口之间互不影响,我行我素,A 窗口卖到第 288 张了,B 窗口不管 A 窗口,自己也卖第 288 张票,那显然会出错了

3 创建线程的三种方式

  • 继承 Thread 类,重写父类 run() 方法

    public class MyThread extends Thread{
    
        @Override
       public void run() {
       	// TODO Auto-generated method stub
       	//super.run();
       	 doSomething();
       }
    
       private void doSomething() {
       	// TODO Auto-generated method stub
       	System.out.println("我是一个线程中的方法");
       }
    }
    
  • 实现 runnable 接口

    public class RunnableThread implements Runnable{
    
       @Override
       public void run() {
       	// TODO Auto-generated method stub
       	doSomeThing();
       }
    
       private void doSomeThing() {
       	// TODO Auto-generated method stub
       	System.out.println("我是一个线程方法");
       }
    
    }
    
  • 使用 ExecutorService、Callable、Future 实现有返回结果的多线程 (JDK5.0 以后)

    public class CallableThread implements Callable<String>{
    
    	@Override
    	public String call() throws Exception {
    		// TODO Auto-generated method stub
    		doSomeThing();
    		return "需要返回的值";
    	}
    
    	private void doSomeThing() {
    		// TODO Auto-generated method stub
    		System.out.println("我是线程中的方法");
    	}
    
    }
    

3.1 采用实现 Runnable、Callable 接口的方式创建多线程的优缺点。

优势:

  • 线程类只是实现了 Runnable 接口与 Callable 接口,还可以继承其他类。
  • 在这种方式下,多个线程可以共享一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势:

  • 编程稍稍复杂,如果需要访问当前线程,则必须使用 Thread.currentThread()方法。

3.2 采用继承 Thread 类的方法创建多线程的优缺点。

优势:

  • 编写简单,如果需要访问当前线程,则无须使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。

劣势:

  • 因为线程类已经继承了 Thread 类,所以不能再继承其他父类。

总结:一般推荐采用实现 Runnable 接口、Callable 接口的方式来创建多线程。

4 线程池

4.1 线程池的作用

线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待为什么要用线程池:。当一个新任务需要运行时,如果线程 池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

4.2 为什么要用线程池

减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务
器累趴下 (每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)

4.3 java 中的四种线程池

  • newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

5 多线程同步机制。

  • 在需要同步的方法的方法签名中加入 synchronized 关键字。
  • 使用 synchronized 块对需要进行同步的代码段进行同步。
  • 使用 JDK 5 中提供的 java.util.concurrent.lock 包中的 Lock 对象。
    一段 synchronized 的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java 里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池 等待队列中)。 取到锁后,他就开始执行同步代码 (被 synchronized 修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中 等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。

6 线程的生命周期

线程在执行过程中,可以处于下面几种状态:
就绪 (Runnable): 线程准备运行,不一定立马就能开始执行。
运行中 (Running):进程正在执行线程的代码。
等待中 (Waiting): 线程处于阻塞的状态,等待外部的处理结束。
睡眠中 (Sleeping):线程被强制睡眠。
I/O 阻塞 (Blocked on I/O):等待 I/O 操作完成。
同步阻塞 (Blocked on Synchronization):等待获取锁。
死亡 (Dead):线程完成了执行

20200520213508170.png

7 什么是守护线程?

守护线程(即 daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以 java 里线程分 2 种,

  1. 守护线程,比如垃圾回收线程,就是最典型的守护线程。

守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连 main 线程也执行完毕,那么 jvm 就会退出(即停止运行)——此时,连 jvm 都停止运行了,守护线程当然也就停止执行了。
再换一种说法,如果有用户自定义线程存在的话,jvm 就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。

  1. 用户线程,就是应用程序里的自定义线程。

应用程序里的线程,一般都是用户自定义线程。
用户也可以在应用程序代码自定义守护线程,只需要调用 Thread 类的设置方法设置一下即可

8 线程锁

多线程可以同时运行多个任务
但是当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误!
so, 不使用线程锁, 可能导致错误。
应用场景:
I/O 密集型操作 需要资源保持同步
优缺点:
优点: 保证资源同步
缺点: 有等待肯定会慢

9 sleep 方法和 wait 方法的区别?

  • 来自不同的类: wait() 方法是 Object 类的方法,sleep 方法是 Thread 类的方法。
  • 对于锁的占用情况不同:最主要是 sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  • 使用范围: wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用)
  • 唤醒方式:调用 sleep() 方法的线程通常是睡眠一定时间后,自动醒来。对象调用 wait() 方法,必须采用 notify() 或者 notifyAll() 方法唤醒。

Sleep() 方法

Sleep() 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中有可能被其他对象调用它的 interrupt(), 产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程就会异常终止,进入 TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行 catch 语句块(可能还有 finally 语句块)以及以后的代码。
注意 sleep() 方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep() 让 t 对象进入 sleep,这样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程。

Wait() 方法

Wait() 属于 Object 的成员方法,一旦一个对象调用了 wait 方法,必须要采用 notify() 和 notifyAll() 方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait() 方法的对象。wait() 方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt() 方法而产生 InterruptedException,效果以及处理方式同 sleep() 方法。

10 什么是死锁 (deadlock)?如何避免死锁?

两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

转自https://blog.csdn.net/xiaobai_mantou/article/details/106245575?utm_medium=distribute.pc_feed.269116.nonecase&depth_1-utm_source=distribute.pc_feed.269116.nonecase


标题:java 中的线程,多线程
作者:shark
地址:https://www.linkjb.com/articles/2020/05/21/1590029003879.html