一、进程和线程

1.1 进程和线程的区别

  1. 根本区别:线程是进程的子集,进程是操作系统资源分配的基本单位,而线程是进程的一个实体,是CPU调度和执行的基本单位,进程是资源分配的最小单位,线程是程序执行的最小单位
  2. 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
  3. 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立
  4. 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

1.2 进程间如何通信

 进程间通信:(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息

 IPC的方式:通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

(1)进程间通信的方式

传统的进程间通信方式

  • 无名管道(pipe)、有名管道(fifo)和信号(signal)

System v IPC对象

  • 共享内存(share memory)、消息队列(message queue)和信号灯(semaphore)
  • 套接字(socket) ——TCP绑定端口

(2)进程通讯的方式总结

  1. 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用进程间的亲缘关系通常是指父子进程关系。
  2. 命名管道(named pipe/FIFO):命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 信号量(semophonre):信号量是一个计数器,可以用来控制多个进程队共享资源的访问。它常作为一个锁机制,防止某进程在访问共享资源时,其他进程也访问此资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  4. 消息队列(message queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 信号(sinal):信号是一种比较复杂的通信方式,用于通知接受进程某个事件已经发生。
  6. 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的ipc通信方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往和其他通信方式如信号量,配合使用来实现进程间的同步和通信。
  7. 套接字(socket):套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备间的进程通信。
  8. 全双工管道:共享内存、信号量、消息队列、管道和命名管道只适用于本地进程间通信,套接字和全双工管道可用于远程通信,因此可用于网络编程。

1.3 线程间如何通信

(1)volatile 共享内存

基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式

(2)等待/通知机制 (wait和notify方法)

Object类提供了线程间通信的方法:wait()notify()notifyaAl(),在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。

(3)基本LockSupport实现线程间的阻塞和唤醒

(4)使用 ReentrantLock 结合 Condition

(5)使用JUC工具类 CountDownLatch 

  • CountDownLatch基于AQS框架,相当于也是维护了一个线程间共享变量state

二、协程

2.1 基本概念

协程,英文Coroutines是一种比线程更加轻量级的存在。用户态的线程,是线程中的线程,切换和调度不需要经过OS(操作系统)。

  • 协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)

2.2 协程与进程、线程的比较

虽然说,协程与进程、线程相比不是一个维度的概念,但是有时候,我们仍然需要将它们做一番比较,具体如下:

  1. 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个维度。
  2. 一个进程可以包含多个线程,一个线程可以包含多个协程。
  3. 一个线程内的多个协程虽然可以切换,但是这多个协程是串行执行的,只能在这一个线程内运行,没法利用CPU多核能力。
  4. 协程与进程一样,它们的切换都存在上下文切换问题。

表面上,进程、线程、协程都存在上下文切换的问题,但是三者上下文切换又有明显不同,见下表:

Java的虚拟线程与golang协程对比 java协程和线程的区别_实现多线程(四种方式)

2.3 协程使用场景

  • Lua语言:Lua从5.0版本开始使用协程,通过扩展库coroutine来实现。
  • Python语言:正如刚才所写的代码示例,python可以通过 yield/send 的方式实现协程。在python 3.5以后,async/await 成为了更好的替代方案。
  • Go语言:Go语言对协程的实现非常强大而简洁,可以轻松创建成百上千个协程并发执行。
  • Java语言:Java语言并没有对协程的原生支持,但是某些开源框架模拟出了协程的功能,有兴趣的小伙伴可以看一看Kilim框架的源码:https://github.com/kilim/kilim

一个线程内的多个协程是串行执行的,不能利用多核,所以,显然,协程不适合计算密集型的场景。协程适合I/O 阻塞型。I/O本身就是阻塞型的(相较于CPU的时间世界而言)。就目前而言,无论I/O的速度多快,也比不上CPU的速度,所以一个I/O相关的程序,当其在进行I/O操作时候,CPU实际上是空闲的。

2.4 协程的优势

  1. 占有的资源少,为什么说他占有资源少呢?举例:操作系统要启一个线程前后要为这个线程配的内存数据大概有          1M,而纤程大概是4K
  2. 由于纤程非常的轻量级,所以切换比较简单
  3.  可以同时被启动很多个(10万个都没问题)

目前支持内置纤程的语言Kotlin Scala Go 等,Java的正在开发中

三、实例分析

 1、继承Thread类,重写run()方法

package com.wxw;
/** 
* @author 作者 wxw
* @version 创建时间:2019年8月15日 下午2:31:31 
* 类说明 
*/
/**
 * 多线程的实现方式(四种)之一
 * 1,继承Thread类,重写run()方法
 * 
 * 说明:
 *   程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。
 *   随着调用ThreadChild的两个对象的start方法,
 *   另外两个线程也启动了,这样,整个应用就在多线程下运行。
 *   。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。
 *    通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一
 *    定会马上得以执行,这取决于CPU调度时机(分时调度和抢占式调度)
 *   
 *  注意:
 *      start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable)
 *      ,什么时候运行是由操作系统决定的。
           从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
 * @author Administrator
 *
 */

public class Thread1 extends Thread {
	  
	  public static void main(String[] args) {
          for (int i = 0; i < 10; i++) {
               //System.out.println(Thread.currentThread().getName() + " " + i);//主线程
              if (i == 0) {
                 ThreadChild child1=new ThreadChild("A");   // 创建一个新的线程 child1 此线程进入新建状态
                 ThreadChild child2=new ThreadChild("B");   // 创建一个新的线程 child2 此线程进入新建状态
            		   child1.start();                   // 调用start()方法使得线程进入就绪状态
            		   child2.start();                   // 调用start()方法使得线程进入就绪状态
                }
           }
       }
		   	
}
/**
 * Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。
 * 启动线程的唯一方法就是通过Thread类的start()实例方法。
 * start()方法是一个native方法,它将启动一个新线程,并执行run()方法。
 * 这种方式实现多线程很简单,通过自己的类直接extend Thread,
 * 并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
 * @author Administrator
 *
 */
class ThreadChild extends Thread{
	 private String name;
	 //无参构造 
	 public ThreadChild() {
		super();
	}
	 //有参构造函数
	public ThreadChild(String name){
		 this.name=name;
	 }
	 	private int i = 0; 
	    @Override
	    public void run() {
	       for (i = 0; i < 10; i++) {
	            System.out.println( name+ "的编号: " + i);//子线程
	        }
	     }
	 
}

 2、实现Runnable接口,并重写该接口的run()方法

package com.wxw;
/** 
* @author 作者 wxw
* @version 创建时间:2019年8月15日 下午3:29:49 
* 类说明 
*/

/**
 * 多线程实现(四种)之二
 * :实现Runnable接口,并重写该接口的run()方法
 * 
 *  说明:
 *   Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法
 *   是多线程程序的一个约定。所有的多线程代码都在run方法里面。
 *   Thread类实际上也是实现了Runnable接口的类。
 * 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 
 * 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
   实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,
   不管是扩展Thread类还是实现Runnable接口来实现多线程,
   最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
 * 
 * @author Administrator
 *
 */

public class RunnableDemo {
    
	  public static void main(String[] args) {
		  
		  for(int i=0;i<10;i++){
			 //System.out.println(Thread.currentThread().getName()+"主线程的编号:"+i);
			 if(i==2){
				 Runnable myrun1=new RunnableChild("A");//创建一个Runnable实现类的对象
				 Runnable myrun2=new RunnableChild("B");//创建一个Runnable实现类的对象
				 Thread thread1=new Thread(myrun1);//将myrun作为Thread 的target创建新线程
				 Thread thread2=new Thread(myrun2);
				 thread1.start();
				 thread2.start();
			 }
		  }
		
	}
}
class RunnableChild implements Runnable{
	
	 private String name;//定义线程名
	 
	public RunnableChild() {
		super();
	}
	//带参构造方法
	public RunnableChild(String name) {
		super();
		this.name = name;
	}
	@Override
	public void run() {
		
        for(int i=0;i<10;i++){
        	System.out.println(name+"的线程编号:"+i);
        }		
	}
	
	
}

3、使用Callable和Future接口创建线程。

package com.wxw;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/** 
* @author 作者 wxw
* @version 创建时间:2019年8月15日 下午3:51:58 
* 类说明 
*/
/**
 * 多线程 实现(四种)之三
 * 
 * 使用Callable和Future接口创建线程。
  a:创建Callable接口的实现类 ,并实现Call方法
  b:创建Callable实现类的实现,使用FutureTask类包装Callable对象,
该FutureTask对象封装了Callable对象的Call方法的返回值
  c:使用FutureTask对象作为Thread对象的target创建并启动线程
  d:调用FutureTask对象的get()来获取子线程执行结束的返回值
 * 
 * 【注意】:在实现Callable接口中,此时不再是run()方法了,而是call()方法,
 * 此call()方法作为线程执行体,
 * 同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,
 * 同时作为了Thread对象的target
 * 
 * @author Administrator
 *
 */


public class CallableDemo {
    
	 public static void main(String[] args) {
		 	
  Callable<Integer> mycable=new myCallable();//创建myCallable 对象
//使用FutureTask来包装mycable对象
FutureTask<Integer> fTask=new FutureTask<Integer>(mycable); 
		
		for(int i=0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+"主线程的编号:"+i);
			if(i==2){
				
				///FutureTask对象作为Thread对象的target创建新的线程
				Thread thread = new Thread(fTask);   
	             thread.start();               //线程进入到就绪状态
			}
		}
		 System.out.println("主线程for循环执行完毕..");
		 try {
	            int sum = fTask.get(); //取得新创建的新线程中的call()方法返回的结果
	        System.out.println("新创建的新线程中的call()方法返回的结果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() throws Exception {
		 int sum=0;
		 for (; i < 10; i++) {
             System.out.println(Thread.currentThread().getName() + " " + i);
             sum += i;
	        }
         return sum;
	}
	
	
	
}

4、通过线程池创建线程

package com.wxw;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/** 
* @author 作者 wxw
* @version 创建时间:2019年8月15日 下午4:22:02 
* 类说明 
*/
/**
 * 多线程实现(四种)之四
 *   【主题】   通过线程池创建线程
 * ExecutorService、Callable都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,
 * 还有Future接口也是属于这个框架,有了这种特征得到返回值就很方便了。
通过分析可以知道,他同样也是实现了Callable接口,实现了Call方法,所以有返回值。
这也就是正好符合了前面所说的两种分类

  执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就
  可以获取到Callable任务返回的Object了。get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
 *
 *再介绍Executors类:提供了一系列工厂方法用于创建线程池,
 *返回的线程池都实现了ExecutorService接口
 *
 *public static ExecutorService newFixedThreadPool(int nThreads)
    创建固定数目线程的线程池。
    public static ExecutorService newCachedThreadPool()
    创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。
    如果现有线程没有可用的,则创建一个新线程并添加到池中。
    终止并从缓存中移除那些已有 60 秒钟未被使用的线 程。
    public static ExecutorService newSingleThreadExecutor()
    创建一个单线程化的Executor。
    public static ScheduledExecutorService newScheduledThreadPool(int
    corePoolSize)
    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
    ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,
    返回Future。如果Executor后台线程池还没有完成Callable的计算,
    这调用返回Future对象的get()方法,会阻塞直到计算完成。
 *
 * @author Administrator
 *
 */
 

public class ThreadPool4 {
    private  static int POOL_NUM=10;//线程池数量
	 
     public static void main(String[] args) throws InterruptedException {
		 ExecutorService service=Executors.newFixedThreadPool(5);
		 for(int i=0;i<POOL_NUM;i++){
			 RunnableThread thread=new RunnableThread();
			 Thread.sleep(100);//睡眠0.1秒
			 service.execute(thread);
			 
		 }
		 //关闭线程池
		 service.shutdown();
	}
}


class RunnableThread implements Runnable{

	@Override
	public void run() { 
	   System.out.println("通过线程池创建线程:"+Thread.currentThread().getName()+" ");
	}
	
	
	
}