Java线程
1. 什么是进程,什么是线程?
答:
进程是资源分配的最小单位,线程是CPU调度的最小单位
进程是操作系统进行资源分配的最小单位,,包括CPU,内存,磁盘…,进程是具有独立功能的程序关于某个数据集上的一次运行活动。分为系统进程和用户进程(用于完成操作系统的各种功能的进程是系统进程,由你启动的进程为用户进程)
线程是CPU调度的最小单位,必须依赖于进程而存在,是进程的一个实体,比进程更小,独立运行,不拥有系统资源,可与同与属于一个进程的其他线程共享进程的全部资源。
两种创建多线程的方式
方式一:继承Thread类
步骤
(1)创建一个继承于Thread的子类
(2)重写Thread类的run()
方法—-》将此线程执行的操作声明在run()
方法中
(3)创建继承于Thread的子类的对象
(4)调用start()
方法(该方法作用:①启动当前线程 ②调用当前线程的run()
)
举例
创建两个线程分别输出偶数和奇数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class MyThread1 extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i%2==0){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } class MyThread2 extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i%2!=0){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } public class ThreadTest1 { public static void main(String[] args) { MyThread1 t1 = new MyThread1(); MyThread2 t2 = new MyThread2(); t1.start(); t2.start(); } }
|
方式二:(实现Runnable接口)
步骤
(1)创建实现Runnable接口的类
(2)实现类去实现Runnable中的抽象方法:run()
(3)创建实现类的对象
(4)将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
(5)通过Thread类的对象调用start()
举例
创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class WindowsThread { public static void main(String[] args) { MyThread my = new MyThread(); Thread t1 = new Thread(my); Thread t2 = new Thread(my); Thread t3 = new Thread(my); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class MyThread implements Runnable{ private int stick = 100; @Override public void run() { while (true){ if (stick>0){ System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+stick); stick--; }else break; } } }
|
两种方式比较
开发中,优先选择,实现Runnable
接口的方式,
原因:(1)实现的方式没有类的单继承的局限性 (2)实现的方式更适合来处理多个线程共享数据的情况
同步
上述卖票过程中,在实现Runnable接口时出现了重复票和错票—》出现了线程安全的问题。
问题出现的原因:当某个线程操作车票时,还没有完成操作,其他线程参与进来也操作车票,导致的不安全。
解决问题:当线程a操作车票时,其他线程不能参与进来,直到a操作完毕后才开始进行下一个线程的操作,
在java中,使用同步机制来解决线程安全的问腿。
同步代码块
1 2 3
| synchronized(同步监视器){ }
|
说明:
(1)操作共享数据的代码,就是要同步的代码
(2)共享数据:多个线程共同操作的变量,比例:车票
(3)同步监视器
:俗称:锁,任何一个对象都可以充当的锁(但是多个线程必须共用一把锁)
继承Runabble接口使用同步代码块的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class MyThread implements Runnable{ private int stick = 100; Object obj =new Object(); @Override public void run() { while (true){ synchronized(obj) { if (stick > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + stick); stick--; } else break; } } } }
|
继承Thread类使用同步代码块的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class Window2 { public static void main(String[] args) { MyThread3 t1 = new MyThread3(); MyThread3 t2 = new MyThread3(); MyThread3 t3 = new MyThread3(); t1.start(); t2.start(); t3.start();
} } class MyThread3 extends Thread{ private int stick = 100; @Override public void run() { while (true){ synchronized(MyThread3.class) { if (stick > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + stick); stick--; } else break; } } } }
|
同步方法
同步方法:就是在操作操作共享数据的方法上加入synchornized
关键字,为同步方法。
实现runnable的方式中,同步方法的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class Window3 { public static void main(String[] args) { MyThread4 t = new MyThread4(); Thread a = new Thread(t); Thread b = new Thread(t); Thread c = new Thread(t); a.start(); b.start(); c.start(); } } class MyThread4 implements Runnable{ private int ticket = 100; @Override public void run() { while (true){ show(); } } public synchronized void show(){ if (ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+ticket); ticket--; } } }
|
继承自Thread抽象类中使用同步方法的代码块为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class window5 { public static void main(String[] args) { MyThread6 t1 = new MyThread6(); MyThread6 t2 = new MyThread6(); MyThread6 t3 = new MyThread6(); t1.start(); t2.start(); t3.start(); } } class MyThread6 extends Thread{ private static int ticket =100; @Override public void run() { while (true){ show(); } } public static synchronized void show(){ if (ticket>0){ try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+ticket); ticket--; } } }
|
关于同步方法的总结
(1)同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。、
(2)非静态同步方法,同步监视器是:this
(3)静态同步方法,同步监视器是:当前类本身。
使用锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class threadLock { public static void main(String[] args) { blank t = new blank(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); } } class blank implements Runnable{ private int ticket = 100; ReentrantLock lock = new ReentrantLock(); @Override public void run() { try { lock.lock(); while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "售票" + ticket); ticket--; } else break; } }finally { lock.unlock(); } } }
|
线程的死锁
死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。
说明:
(1)出现死锁后,不会出现异常,不会出现提示,只是所有线程都处于阻塞的状态,无法继续。 (2)我们出现同步时,要避免出现死锁。
例题:
- 第一个线程执行完s1之后休眠,当时间过了之后要拿到s2的锁,第二个线程执行完s2之后休眠,当时间过了之后要拿到s1的锁
- 但是目前s1和s2的锁都在等待,都在等待对方放弃自己需要的同步资源,因此,形成了死锁。
代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| public class thread { public static void main(String[] args) {
StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append(1); try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (s2){ s1.append("b"); s2.append(2);
System.out.println(s1); System.out.println(s2); } }
} }.start(); new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append(3); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (s1){ s1.append("d"); s2.append(4); } System.out.println(s1); System.out.println(s2); } } }).start();
}
}
|
死锁必须具备以下四个条件:
- 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
- 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕
后才释放资源。
- 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
如何避免线程死锁? :破坏四个条件
- 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件 :⼀次性申请所有的资源。
- 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
对线程 2 的代码修改成下⾯这样就不会产⽣死锁了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| new Thread(() -> { synchronized (s1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting getresource2"); synchronized (s2) { System.out.println(Thread.currentThread() + "getresource2"); } } }, "线程 2").start();
|
单例模式
饿汉式创建单例模式(本身就是线程安全的,在类加载时才会初始化)
1 2 3 4 5 6 7 8
| class bank2{ private static bank2 b = new bank2(); private bank2(){} public static bank2 getInstance(){ return b; } }
|
线程安全的懒汉式单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class bank1{ private bank1(){}; private static bank1 instance = null; public static bank1 getInstance(){ if (instance==null){ synchronized (bank1.class){ if (instance==null){ instance = new bank1(); ; } } } return instance; } }
|
总结
面试题:synchronized和lock的异同:
(1)相同:二者都可以解决线程安全问题
(2)不同:sychronized机制在执行完相应的同步代码以后,自动释放同步监视器
lock需要手动启动同步(lock()
),同时结束也需要手动实现(unlock()
)
使用的优先顺序:
lock—->同步代码块(已经进入方法体,分配了相应的资源)—->同步方法(在方法体之外)
利弊:
好处:解决线程安全问题
坏处:操作同步代码块时,只能一个线程参与,其他线程等待,相当于一个单线程的过程,效率低
线程之间的通信
举例
要求线程交替打印100以内的数字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class CommunicationTest { public static void main(String[] args) { communicateThread a = new communicateThread(); Thread t1 = new Thread(a); Thread t2 = new Thread(a); t1.start(); t2.start();
} } class communicateThread implements Runnable{ private int num = 1; @Override public synchronized void run() { while (true){ if (num<=100) { notify(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "打印" + num); num++; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else break; } } }
|
wait,notify,notifyAll
涉及到的三个方法:
(1)wait()
:一旦执行此方法,当前前程进入阻塞状态,并释放同步监视器。
(2)notify()
:一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,唤醒优先级高的。
(3)notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1.wait()
,notify()
,notifyAll()
三个方法,必须使用在同步代码快或者同步方法中。
2.wait()
,notify()
,notifyAll()
三个方法,调用者必须是同步代码块或同步方法中的同步监视器。
面试题:
sleep()
,wait()
两个方法的异同:
1.相同点:一旦执行方法,都可以使得当前线程进入阻塞状态。
2.不同点:(1)声明位置不同:Thread类中声明sleep()
,Object类中声明wait()
。
(2)调用的方法不同:sleep()
可以在任何需要的场景下调用,而wait()
必须在同步代码块或同步方法中调用。
(3)关于释放同步监视器,:两个方法都使用在同步代码块或同步方法中,sleep()
不会释放锁,wait()
会释放锁
(4)wait()
通常用于线程交互/通信,sleep()
通常用于暂停执行
(5)wait()
方法调用后,线程不会自动苏醒,需要别的线程调⽤同⼀个对象上的 notify()
或者 notifyAll()
⽅法。 sleep()
⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout)
超时后线程会⾃动苏醒
练习: 例题目:生产者和消费者,生产者生产产品给柜员,柜员最多拥有20个产品,如果大于20,则生产者停止生产,消费者消费产品,如果柜员没有产品,则进行等待
分析:
线程:生产者和消费者
共享数据:柜员或产品
是否有线程安全问题:有
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| public class ProductCustomerTest { public static void main(String[] args) { clerk c = new clerk(); product p = new product(c); customer c1 = new customer(c); p.setName("生产者1"); c1.setName("消费者1"); p.start(); c1.start(); } }
class product extends Thread{ private clerk c; public product(clerk c){ this.c = c; }
@Override public void run() { while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } c.productClert(); } } }
class customer extends Thread{ private clerk c; public customer(clerk c){ this.c = c; }
@Override public void run() { while (true){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } c.customerClert(); } } }
class clerk{ private int num = 0; public synchronized void productClert(){ if (num<20){ num++; System.out.println(Thread.currentThread().getName()+"开始生产第"+num+"个产品"); notify(); }else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void customerClert(){ if (num>0){ System.out.println(Thread.currentThread().getName()+"开始消费第"+num+"个产品"); num--; notify(); }else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }
}
|
JDK5.0新增线程创建方式
实现Callable接口
由上面的图片可以看出FutureTask
类是实现了Runnable
接口的。上面那一种方式正式实现Runnable
接口并将
对象作为Thread
的参数。那么显而易见可以new FutureTask()
作为Thread
的参数实现创建线程。
不同的是FutureTask
类需要一个参数Callable
接口类型的对象(这时就只需要创建一个Callable
的对象,
使用匿名内部类,Lambda表达式,或者创建一个类实现Callable接口 其一就能得到Callable
类型的对象)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import java.util.concurrent.*;
public class Main2 { public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask(new Callable() { @Override public Object call() throws Exception { int a = 0; return a; } }); Thread thread = new Thread(futureTask); thread.start(); try { Integer a = futureTask.get(); System.out.println("a="+a); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;
public class ThreadCallable { public static void main(String[] args) { NumThread thread = new NumThread(); FutureTask futureTask = new FutureTask(thread); new Thread(futureTask).start(); try { Object sum = futureTask.get(); System.out.println("总和为:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }
} }
class NumThread implements Callable{ private int sum; @Override public Object call() throws Exception { for (int i = 1;i<=100;i++){ if (i%2==0) { System.out.println(i); sum = sum + i; } } return sum; } }
|
如何理解Callable接口的方式创建多线程比实现Runnable接口创建多线程的方式强大
(1)call()可以有返回值
(2)call()可以抛出异常,被外面捕获,获取异常信息
(3)Callable是支持泛型的
线程池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPool { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); ThreadPoolExecutor service = (ThreadPoolExecutor) executorService;
executorService.execute(new NumThread2());
executorService.shutdown(); } } class NumThread2 implements Runnable{ @Override public void run() { for (int i = 1;i<=100;i++){ if (i%2==0){ System.out.println(i); } } } }
|
使用线程池的好处:
(1)提高响应速度(减少创建新线程的时间)
(2)降低资源的消耗(重复利用线程池中的线程,不需要每次都创建)
(3)便于线程管理:
corePoolSize
:核心池的大小
maximumPoolSize
:最大线程数
keepAliveTime
:线程没有任务时最多保持多长时间会终止
**线程池的创建推荐
ThreadPoolExecutor方式(阿里推荐)
1,ThreadPoolExecutor的实例化七个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| java.util.concurrent.ThreadPoolExecutor类的构造器(其一):
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ?null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
|
构造器七个参数:
[1] corePoolSize:线程池中的常驻核心线程数
[2] maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
[3] keepAliveTime:多余的空闲线程的存活时间。当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime的线程会被销毁,直到剩余线程数量等于 corePoolSize
[4] unit:keepAliveTime的时间单位
[5] workQueue:任务队列,被提交但尚未被执行的任务
[6] threadFactory:表示生成线程池中工作线程的工厂, 用于创建线程,一般默认的即可
[7] handler:拒绝策略处理器。当任务队列已满,工作线程也达到了maximumPoolSize,新增的工作任务将按照某个既定的拒绝策略被拒绝执行。
2. 其中任务队列的类型(BlockingQueue接口),有七个实现类,创建线程时根据业务选择
1 2 3 4 5 6 7 8 9 10 11 12 13
| [1] ArrayBlockingQueue:由数组结构组成的有界阻塞队列。 [2] LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。 [3] PriorityBlockingQueue:支持优先级排序的无界阻塞队列。 [4] DelayQueue:使用优先级队列实现的延迟无界阻塞队列。 [5] SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。 [6] LinkedTransferQueue:由链表组成的无界阻塞队列。 [7] LinkedBlockingDeque:由链表组成的双向阻塞队列。
|
3. 拒绝策略处理器,创建线程时根据业务选择
一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但这种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定`ThreadPoolExecutor`的`RejectedExecutionHandler`参数即合理的拒绝策略,来处理线程池“超载”的情况。
ThreadPoolExecutor自带的拒绝策略如下:
[1] AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
[2] CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
[3] DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
[4] DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略。
使用ThreadPoolExecutor方式手写线程池:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import java.util.concurrent.*;
public class Main2 { public static void main(String[] args) { int corePoolSize =10; int maximumPoolSize = 20; long keepAliveTime = 1000; TimeUnit unit = TimeUnit.SECONDS; BlockingDeque workQueue = new LinkedBlockingDeque<>(3); ThreadFactory threadFactory = Executors.defaultThreadFactory(); RejectedExecutionHandler policy =
new ThreadPoolExecutor.DiscardOldestPolicy(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, policy); threadPoolExecutor.execute(new Runnable() { @Override public void run() { while (true){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在工作"); } } });
} }
|
submit()和execute方法区别
1.submit() 方法可以配合 Future来接收线程执行的返回值,而 execute() 不能接收返回值;
2.execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法。