多线程
第一节 进程和 线程
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~50、51~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),可以改进性能。