Day19-20多线程

多线程

第一节 进程和 线程
1.1 进程的介绍
	是一个程序的运行状态和资源占用(内存,CPU)的描述
	进程是程序的一个动态过程,它指的是从代码加载到执行完毕的一个完成过程
	
	进程的特点:
		a.独立性:不同的进程之间是独立的,相互之间资源不共享(举例:两个正在上课的教室有各自的财产,相互之间不共享)
		b.动态性:进程在系统中不是静止不动的,而是在系统中一直活动的
		c.并发性:多个进程可以在单个处理器上同时进行,且互不影响
1.2 线程的介绍
	是进程的组成部分,一个进程可以有多个线程,每个线程去处理一个特定的子任务

	线程的执行是抢占式的,多个线程在同一个进程中可以并发执行,其实就是CPU快速的在不同的线程之间切换,也就是说,当前运行的线程在任何时候都有可能被挂起,以便另外一个线程可以运行
1.3 进程和线程的关系以及区别
a.一个程序运行后至少有一个进程
b.一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的
c.进程间不能共享资源,但线程之间可以
d.系统创建进程需要为该进程重新分配系统资源,而创建线程则容易的多,因此使用线程实现多任务并发比多进程的效率高
e.系统创建进程需要为该进程重新分配系统资源,而创建线程则容易的多,因此使用线程实现多任务并发比多进程的效率高
第二节 多线程的实现
2.1 继承Thread类
继承自Thread类,Thread类是所有线程类的父类,实现了对线程的抽取和封装

继承Thread类创建并启动多线程的步骤:
		a.定义一个类,继承自Thread类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此,run方法的方法体被称为线程执行体
		b.创建Thread子类的对象,即创建了子线程
		c.用线程对象的start方法来启动该线程

代码实现:

public class ThreadUsageDemo01 {
	public static void main(String[] args) {
		
		//实际的子线程
		MyThread t0 = new MyThread();
		t0.setName("线程000");
		t0.start();
		
		MyThread t1 = new MyThread();
		t1.setName("线程111");
		t1.start();	
		
		/**
		 * static Thread currentThread() 
          	返回对当前正在执行的线程对象的引用。 
		 */
		//这个方法的调用在哪个线程的线程执行体中,则指的是哪个当前正在执行的线程
		Thread thread = Thread.currentThread();
		System.out.println(thread);//Thread[main,5,main]
		//Thread[Thread-0,5,main]
		//Thread[Thread-1,5,main]
		
		//Thread[线程的名字,线程的执行优先级,在哪个线程中创建的]
		
		System.out.println(thread.getName());//main
		
		//设置线程的名字
		thread.setName("主线程");
		
		System.out.println(thread.getName());
		
		//通过构造方法设置线程的名字
		MyThread1 t2 = new MyThread1("新的线程~~~~");
		t2.start();
	}
}
//线程类
class MyThread extends Thread {
	@Override
	public void run() {
		
		for(int i = 0;i < 10;i++) {
			System.out.println("hello" + i);
		}
		
		Thread thread = Thread.currentThread();
		System.out.println(thread);
		System.out.println(thread.getName());
	}
}

class MyThread1 extends Thread {
	public MyThread1() {}
	public MyThread1(String name) {
		super(name);//调用的父类中的Thread(String name)
	}
	@Override
	public void run() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName());
	}
}

​ 案例:模拟售票员售票

public class ThreadTextDemo01 {
	public static void main(String[] args) {
		//需求:模拟4个售票员售100张票
		SellTickets s1 = new SellTickets();
		SellTickets s2 = new SellTickets();
		SellTickets s3 = new SellTickets();
		SellTickets s4 = new SellTickets();
		
		s1.start();
		s2.start();
		s3.start();
		s4.start();
	}
}
class SellTickets extends Thread {
	//共享数据
	static int count = 100;
	@Override
	public void run() {
		//循环售票
		while(count > 0) {
			count--;
			System.out.println(Thread.currentThread().getName() + "售出了一张票,剩余" + count);
		}
	}
}
2.2 实现Runnable接口
实现Runnable接口创建并启动多线程的步骤:

		a.定义一个Runnable接口的实现类,并重写该接口中的run方法,该run方法的方法体同样是该线程的线程执行体
		b.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
		c.调用线程对象的start方法来启动该线程    

public class ThreadUsageDemo02 {

	public static void main(String[] args) {
		//并不是线程对象
		Check c = new Check();
		
		/**
		 * Thread(Runnable target) 
          	分配新的 Thread 对象。
		 */
		Thread t0 = new Thread(c);
		t0.start();
		
		Thread t1 = new Thread(c);
		t1.start();
	}
}
//实现类
class Check implements Runnable {
	@Override
	public void run(){
		for(int i = 0;i < 10;i++) {
			System.out.println(i);
		}
	}
}

​ 案例:模拟售票员售票

public class ThreadTextDemo02 {
	static int count = 100;
	static Runnable r = new Runnable() {
		
		@Override
		public void run() {
			while(count > 0) {
				count--;
				System.out.println(Thread.currentThread().getName() + "售出了一张票,剩余" + count);
			}
		}
	};

	public static void main(String[] args) {
		Thread t0 = new Thread(r);
		Thread t1 = new Thread(r);
		Thread t2 = new Thread(r);
		Thread t3 = new Thread(r);
		
		t0.start();
		t1.start();
		t2.start();
		t3.start();
	}
}

2.3 两种实现方式的比较
实现Runnable接口的方式
	a.线程类只是实现了Runnable接口,还可以继承其他类【一个类在实现接口的同时还可以继承另外一个类】
	b.可以多个线程共享同一个target对象,所以非常适合多个线程来处理同一份资源的情况
	c.弊端:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()

继承Thread类的方式
		a.编写简单,如果要访问当前线程,除了可以通过Thread.currentThread()方式之外,还可以使用super关键字
		b.弊端:因为线程类已经继承了Thread类,则不能再继承其他类【单继承】

	实际上大多数的多线程应用都可以采用实现Runnable接口的方式来实现【推荐使用匿名内部类】 

2.4 调用start()与run()方法的区别
	当调用start()方法时将创建新的线程,并且执行run()方法里的代码,但是如果直接调用start()方法,不会创建新的线程也不会执行调用线程的代码    

第三节 线程的常用方法
3.1 设置线程的名称
Thread t1 = new Thread();
t1.setName("线程1");
3.2 线程休眠
	使得当前正在执行的线程休眠一段时间,释放时间片,导致线程进入阻塞状态

	sleep(5000),5000的单位是毫秒,设置了sleep就相当于将当前线程挂起5s,这个操作跟线程的优先级无关,当对应的时间到了之后,还会再继续执行

代码实现:

public class ThreadFunctionDemo01 {
	static Runnable r = new Runnable() {
		
		@Override
		public void run() {
			while(true) {
				System.out.println(Thread.currentThread().getName() + "在执行");
			
				//设置线程休眠
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	};

	public static void main(String[] args) {
		Thread t0 = new Thread(r);
		t0.setName("线程000");
		t0.setPriority(8);
		t0.start();
		
		Thread t1 = new Thread(r);
		t1.setName("线程111");
		t1.setPriority(3);
		t1.start();
	}
}

3.4 线程让步
	可以让当前正在执行的线程暂停,但它不会阻塞该线程,他只是将该线程转入就绪状态,完全可能出现的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行

	实际上,当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程才会获得执行的机会

代码实现:

public class YieldFunctionDemo01 {
	public static void main(String[] args) {
		YieldThread t0 = new YieldThread("线程000");
		//t0.setPriority(8);
		t0.start();
		
		YieldThread t1 = new YieldThread("线程111");
		t1.start();
	}

}
class YieldThread extends Thread {
	public YieldThread(){}
	public YieldThread(String name) {
		super(name);
	}
	@Override
	public void run() {
		for(int i = 0;i < 50;i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			
			if(i==20) {
				//线程让步,不会让线程进入阻塞状态
				Thread.yield();
			}
		}
	}
}

第四节:线程的状态

基本:

在这里插入图片描述

等待:在这里插入图片描述

阻塞:在这里插入图片描述

第五节:线程安全的问题
5.1线程安全问题:在这里插入图片描述

需求:A线程将“Hello”存入数组的第一个空位,B线程将“World”存入数组的第一个空位

线程不安全:

当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。

临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。

原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。

5.2同步解决线程安全问题:
同步方式  1:
同步代码块:
synchornized(临界资源对象){//对临界资源对象加锁
	//代码(原子操作)
}
注:
	每个对象都有一个互斥锁标记,用来分配给线程的。
	只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
	线程退出同步代码块时,会释放相应的互斥锁标记。

同步方式   2:
同步方法:
synchronized 返回值类型 方法名称(形参列表0){ //对当前对象(this)加锁
	// 代码(原子操作)
}

注:
	只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
	线程退出同步方法时,会释放相应的互斥锁标记。

同步规则:
注意:
只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。

已知JDK中线程安全的类:
StringBuffer
Vector
Hashtable
以上类中的公开方法,均为synchonized修饰的同步方法。

第六节 死锁
6.1死锁:**

当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。

一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。


public class TestDeadLock {
	public static void main(String[] args) throws InterruptedException {
		Chopstick cp = new Chopstick();
		
		Thread t1 = new Thread(new Boy());
		Thread t2 = new Thread(new Girl());
		t2.start();
		
		//Thread.sleep(5000);//间隔5秒,让t2拿到所有资源
		t1.start();
		
		
		
	}
}	

class Chopstick{
	public static Object left = "左快子";
	public static Object right = "有筷子";
}

class Boy implements Runnable{
	@Override
	public void run() {
		synchronized (Chopstick.left) {
			System.out.println("男孩拿到了左筷子");
			synchronized (Chopstick.right) {
				System.out.println("男孩拿到了右筷子");
				System.out.println("开始吃饭");
				System.out.println("男孩吃完啦!");
			}
		}
	}
}
class Girl implements Runnable{

	@Override
	public void run() {
		synchronized (Chopstick.right) {
			System.out.println("女孩拿到了右筷子");
			synchronized (Chopstick.left) {
				System.out.println("女孩拿到了左筷子");
				System.out.println("开始吃饭");
				System.out.println("女孩吃完啦!");
			}
		}
	}
	
}
6.2消费者与生产者

生产者、消费者:

若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。

实现方式:

采用wait()notify()方法

wait():当缓冲区已满或空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等待状态,让其他线程执行

		·是Object的方法
		·调用方式:对象.wait();
		·表示释放 对象 这个锁标记,然后在锁外边等待(对比sleep(),sleep是抱着锁休眠的)
		·等待,必须放到同步代码段中执行	
		
notify():当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态

		·是Object的方法
		·调用方式:对象.notify();
		·表示唤醒 对象 所标记外边在等待的一个线程
		
		public static void main(String[] args) {
		//1.创建商品对象
		Shop shop = new Shop();
		//2.创建消费者和生产者线程对象
		Thread product = new Thread(new Product(shop),"商家");
		Thread customer = new Thread(new Customer(shop),"买家");
		//3.进行消费和生成
		product.start();
		customer.start();
	}
}
//商品
class Goods{
	private int id;
	
	public Goods() {
		super();
		// TODO Auto-generated constructor stub
	}

	public Goods(int id) {
		super();
		this.id = id;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}

}
class Shop{
	private boolean flag;//标识是否有商品
	private Goods goods;//商品
	public synchronized void saveGoods(Goods g){
		if(flag == true){
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.goods  = g;
		System.out.println(Thread.currentThread().getName()+"往商场里存了"+g.getId()+"件商品");
		flag = true;//标识已经存放了商品
		this.notify();
	}
	public synchronized void saleGoods(Goods g){
		if(!flag == true){//没有商品 flag是false,但是为了让消费者等待,取反值,执行wait();
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		this.goods = g;
		this.flag = false;
		System.out.println(Thread.currentThread().getName()+"在商场里购买了"+g.getId()+"件商品");
		this.notify();
	}
}
class Product implements Runnable{
	Shop shop;
	@Override
	public void run() {
		for (int i = 1; i <= 50; i++) {
			shop.saveGoods(new Goods(i));
		}
	}
	public Product(Shop shop) {
		super();
		this.shop = shop;
	}
	
}
class Customer implements Runnable{
	Shop shop;
	
	@Override
	public void run() {
		for (int i = 1; i <=50; i++) {
			shop.saleGoods(new Goods(i));
		}
	}

	public Customer(Shop shop) {
		super();
		this.shop = shop;
	}
	
	
第七节:高级多线程

现有问题:

线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。

频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降。

线程池:

线程容器,可设定线程分配的数量上限。

将预先创建线程对象存入池中,并重用线程池中的线程对象。

避免频繁的创建和销毁。

线程池原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Z52fxoE-1577616548143)(C:\Users\aqiang\Desktop\一阶段\Day15\线程池原理.png)]

7.1获取线程池
常用的线程池接口和类(所在包java.util.concurrent):
Executor:线程池的顶级接口。

ExecutorService:线程池接口,可通过submit(Runnable task) 提交任务代码。

 Executors工厂类:通过此类可以获得一个线程池。

 通过 newFixedThreadPool(int nThreads)  获取固定数量的线程池。参数:指定线程池中线程的数量。

通过newCachedThreadPool() 获得动态数量的线程池,如不够则创建新的,没有上限

7.2Callable接口
public interface Callable<V>{
public V call() throws Exception;
}

JDK5加入,与Runnable接口类似,实现之后代表一个线程任务。
Callable具有泛型返回值、可以声明异常。

7.3Future接口
应用场景:
需求:使用两个线程,并发计算1~5051~100的和,再进行汇总统计。
概念:异步接收ExecutorService.submit()所返回的状态结果,当中包含了call()的返回值

方法:V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)



public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService es =  Executors.newFixedThreadPool(2);
		Future<Integer> result1 =  es.submit(new Calc1());//1~50	
		Future<Integer> result2 = es.submit(new Calc2());//51~100
		int a = result1.get();//通过future的get方法,获取任务执行完毕后的结果
		int b = result2.get();//通过future的get方法,获取任务执行完毕后的结果
		System.out.println(a+b);
		
	}
}
class Calc1 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		int sum = 0;
		for(int i=1;i<=50;i++){
			sum = sum + i;
		}
		System.out.println(sum);
		return sum;
	}
}
class Calc2 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		int sum = 0;
		for(int i=51;i<=100;i++){
			sum = sum + i;
		}
		System.out.println(sum);
		return sum;
}
第八节:线程的同步和异步

在这里插入图片描述
在这里插入图片描述

第九节:Lock接口
JDK5加入,与synchronized比较,显示定义,结构更灵活。

提供更多实用性方法,功能更强大、性能更优越。

常用方法:
void lock() //获取锁,如锁被占用,则等待。
boolean tryLock() //尝试获取锁(成功返回true。失败返回false,不阻塞)
void unlock() //释放锁

Lock接口的实现类:ReentrantLock,与synchornized一样具有互斥锁功能
读写锁:
ReentrantReadWriteLock:
一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。
支持多次分配读锁,使多个读操作可以并发执行。

互斥规则:
写-写:互斥,阻塞。
读-写:互斥,读阻塞写、写阻塞读。
读-读:不互斥、不阻塞。
在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。

第十节:线程安全的集合

在这里插入图片描述

10.1Collections工具的方法:
Collections工具类中提供了多个可以获得线程安全集合的方法。

public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> list)
public static <T> Set<T> synchronizedSet(Set<T> s)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)

JDK1.2提供,接口统一、维护性高,但性能没有提升,均以synchonized实现
10.2 CopyOnWriteArrayList
线程安全的ArrayList,加强版读写分离。
写有锁,读无锁,读写之间不阻塞,优于读写锁。
写入时,先copy一个容器副本、再添加新元素,最后替换引用。
使用方式与ArrayList无异。
List<String> list = new CopyOnWriteArrayList<String>();
10.3 CopyOnWriteArraySet
线程安全的Set,底层使用CopyOnWriteArrayList实现。
唯一不同在于,使用addIfAbsent()添加元素,会遍历数组,
如存在元素,则不添加(扔掉副本)。

Set<String> slist = new CopyOnWriteSet<String>();
10.4 ConcurrentHashMap
JDK  1.7
初始容量默认为16段(Segment),使用分段锁设计。
不对整个Map加锁,而是为每个Segment加锁。
当多个对象存入同一个Segment时,才需要互斥。
最理想状态为16个对象分别存入16个Segment,并行数量16。
使用方式与HashMap无异。
Map<String,String> maps  =  new ConcurrentHashMap<String,String>();

JDK  1.8
在jdk1.8中主要做了2方面的改进

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

tring> slist = new CopyOnWriteSet();


##### 10.4  ConcurrentHashMap

JDK 1.7
初始容量默认为16段(Segment),使用分段锁设计。
不对整个Map加锁,而是为每个Segment加锁。
当多个对象存入同一个Segment时,才需要互斥。
最理想状态为16个对象分别存入16个Segment,并行数量16。
使用方式与HashMap无异。
Map<String,String> maps = new ConcurrentHashMap<String,String>();

JDK 1.8
在jdk1.8中主要做了2方面的改进

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值