多线程
一、线程与进程
A、进程:
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。比如一个独立运行的软件
B、线程:
(1)、是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少有一个线程
(2)、线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
C、线程调度
1、分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
2、抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
**注意:**CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。
二、同步、异步、并发、并行
同步:排队执行 , 效率低但是安全。
异步:同时执行 , 效率高但是数据不安全。
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
三、实现多线程技术
A、Thread类:
用于开启一个新的线程,当开启一个新的执行路径时,需要去继承Thread类。并且需要重写Thread类中的run()方法。
示例:
//继承Thread类
public class MyThread extends java.lang.Thread {
/**
* 继承Thread类之后需要重写run方法
* run方法中的执行内容就是线程要执行的线程方法
*/
@Override
public void run() {
//是一条新的执行路径
//这个方法的触发方式,不是直接调用这个方法,而是需要i盗用start()方法来启动任务
for (int i = 0 ;i<10;i++){
System.out.println("哈哈哈"+i);
}
}
}
//main方法
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i = 0 ;i<10;i++){
System.out.println("嘿嘿嘿"+i);
}
}
结果:
图解:
B、Runnable接口
1、创建一个类实现Runnable接口中的run()方法。
代码:
public class Runnable implements java.lang.Runnable {
@Override
public void run() {
//实现Runnable中的run()方法。
for (int i = 0 ;i<10;i++){
System.out.println("哈哈哈"+i);
}
}
}
2、在main()方法中创建一个实现Runnable接口的类的对象和一个Thread对象。
3、将实现Runnable接口的类的对象传递给Thread类的start()方法。
代码:
Rnnable r = new Runnable(); //实现Runnable接口的类
Thread t = new Thread();
t.start();
for (int i = 0 ;i<10;i++){
System.out.println("哈哈哈"+i);
实现Runnable和继承Thread类的优点:
1、通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况
2、可以避免单继承带来的局限性
3、任务与线程本身是分离的,提高了程序的健壮性
4、线程池只接受Runnable类型的任务,不接受Thread类型的任务
获取当前线程:Thread.currentThread()
获取和设置名称:getname()、setname()
C、线程阻塞
可以理解为所有的耗时操作即为线程阻塞。
D、线程中断
1、一个线程是一个独立的执行路径,他是否应该结束,应该有自身来决定。
先使用interrupt()方法打上标记,然后当发现标记时return。
示例:
public class Dead {
public static void main(String[] args) throws InterruptedException {
Runnalbe r = new Runnalbe();
Thread t1 = new Thread(r);
t1.start();
for (int i = 0 ;i<6;i++){
System.out.println(Thread.currentThread().getName()+i);
Thread.sleep(1000);
}
t1.interrupt(); //为t1线程打上标记
}
static class Runnalbe implements Runnable{
@Override
public void run() {
for (int i = 0 ;i<10;i++){
System.out.println(Thread.currentThread().getName()+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("main方法结束,我也要结束");
return; //当发现线程被打上标记之后,就执行return跳出循环。
}
}
}
}
}
结果:
四、守护线程
线程分为守护线程和用户线程。
用户线程:当一个进程不包含任何存存活的用户线程时,此进程结束。直接创建的线程都是用户线程
守护线程:为了守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
将创建的线程设置为守护线程:调用setDaemon()方法
t1.setDaemon(true); //设置守护线程
五、线程安全
不安全的原因:因为多个线程同时执行,去争抢一个数据导致数据在使用时和最初时不一样。
A、同步代码块:
格式:
synchronized (锁对象){} //java中任何对象都可以作为锁对象,针对同一把锁
示例:
public static void main(String[] args) {
//synchronized (所对象){}
java.lang.Runnable r = new Ticket();
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
}
static class Ticket implements Runnable{
private int count = 10;
private Object o = new Object();
@Override
public void run() {
while(true) {
synchronized (o) {
if (count > 0) {
System.out.println("正在出票......");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票为:" + count);
}
}
}
}
}
结果:当count<0时,程序就自动停止了
B、同步方法
将synchronized关键字修饰方法。
-
public static void main(String[] args) { //synchronized (锁对象){} Runnable r = new Ticket(); new Thread(r).start(); new Thread(r).start(); new Thread(r).start(); } static class Ticket implements Runnable { private int count = 10; private Object o = new Object(); @Override public void run() { while (true) { boolean flag = sale(); if(!flag){ break; } } } public synchronized boolean sale() { if (count > 0) { System.out.println("正在出票......"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "出票成功,余票为:" + count); return true; }else{ return false; } } }
结果:
C、显示锁Lock
同步方法和同步代码块属于隐式锁。
示例:创建一个锁对象,然后调用lock方法进行上锁,执行结束之后调用unlock方法进行解锁。
public static void main(String[] args) {
//
Runnable r = new Ticket();
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
}
static class Ticket implements Runnable {
private int count = 10;
// private Object o = new Object();
private Lock l = new ReentrantLock(); //创建锁对象
@Override
public void run() {
while (true) {
l.lock(); //上锁
if (count > 0) {
System.out.println("正在出票......");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "出票成功,余票为:" + count);
}
l.unlock(); //解锁
}
}
}
结果:
D、公平锁和非公平锁:
E、显示锁和隐式锁的区别:
1、出身不同:
-
synchronized:Java中的关键字,是由JVM来维护的,是JVM层面的锁。
(1)、synchronized底层是通过monitorenter进行加锁,底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。并且只有在同步块或同步方法中,JVM才会调用monitory对象的,才可以调用wait/notify等方)
(2)、通过monitorexit来退出锁
-
Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API,是API层面的锁。lock是通过调用对应的API方法来获取锁和释放锁的。
2、使用方法不同:
(1)、synchronized是自动获取锁和解除锁,由系统维护,若非逻辑原因,不会造成死锁。
(2)、lock是一个类,需要手动进行过上锁和解锁,若没有即使解锁会造成死锁,上锁:lock()方法;解锁:unlock()方法。
3、等待是否可被中断:
(1)、synchronized不可中断,除非抛出异常或者正常运行完成。
(2)、lock可以中断的。
lock中断方式:调用设置超时方法tryLock(long timeout ,timeUnit unit);调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
4、上锁时是否可被设置为公平锁:
(1)、synchronized不可以。
(2)、lock都可以,默认为非公平锁。
5、锁绑定多个条件来condition:
(1)、synchronized不能精确唤醒线程。要么随机唤醒一个线程;要么是唤醒所有等待的线程。。
(2)、lock用来实现分组唤醒需要唤醒的线程,可以精确的唤醒。
6、性能区别:
lock写控制锁的代码,性能高。
F、线程锁死
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
原因:
1、系统资源的竞争
2、进程推进顺序非法
3、 死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
- 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图2-15所示。
G、生产者与消费案例:
示例:厨师在做菜结束时,将flag设为flase,唤醒其余线程并且让自己进入等待状态;当服务员线程被唤醒之后,开始传送菜品,传送结束时,将flag设为true,唤醒其余线程并且让自己进入等待状态。
public class Product {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i = 0;i<100;i++){
if(i%2==0){
f.setNameAndTaste("麻辣香锅","麻辣味");
}else{
f.setNameAndTaste("猪肉粉条","酸味");
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
for (int i= 0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
@Override
public void run() {
super.run();
}
}
//食物类
static class Food{
private String name;
private String taste;
private boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste) {
if (flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get() {
if (!flag) {
System.out.println("服务员端走的菜名是:" + name + ",味道是:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果:
Callable创建线程:
示例:
public class Callable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
java.util.concurrent.Callable<Integer> c = new Mycallable();
FutureTask f = new FutureTask(c);
new Thread(f).start();
Integer r = (Integer) f.get();
System.out.println("返回值:"+r);
for(int i = 0; i <10;i++){
Thread.sleep(1000);
System.out.println(i);
}
}
static class Mycallable implements java.util.concurrent.Callable<Integer>{
@Override
public Integer call() throws Exception {
for(int i = 0; i <10;i++){
Thread.sleep(1000);
System.out.println(i);
}
return 100;
}
}
}
结果:
六、线程池
好处:
降低资源消耗。
提高响应速度。
提高线程的可管理性。
执行流程:
-
判断线程池是否存在空闲线程
-
存在则使用
-
不存在,则创建线程 并放入线程池, 然后使用
注:
显示锁和隐式锁的区别参考自:https://blog.csdn.net/qq_43570075/article/details/106243873