在学习多线程以前,我们所写的大部分程序只开发了电脑强大的cpu一部分的算力,在运行迭代或者循环体量大的代码时会显得效率低下,多线程就像通过追加同时操作的通道来达到深度开发cpu算力的效果,让cpu从摸鱼到加班。(黑心老板竟是我自己)
1. 多线程的有关概念
1.线程与进程
线程被称为轻型进程,其被包含在进程中,一条进程必须有一条以上的线程
堆:作为内存存储大量数据,包括我们新new的对象。
方法区:存储的便是类中的信息,属性,方法等
一条进程有多条线程,在该任务有大部分时间使cpu休息的情况下,多线程使cpu在不同的线程跳转运算,可以增加cpu的效率,(黑老板压榨员工剩余价值)(大雾)
2.线程调度
定义:根据Jvm虚拟机的调度方式,来给多个线程分配cpu的使用时间片
调度方式:
-
分时调度,让每个线程平均分配cpu的使用权,且分配时间片也相同。
这是某个自律的人的任务进程表,这里可以看到自律的人有多可怕,有条不紊,可惜的是没有充分利用cpu(大脑),运动双线程之类的
-
抢占式调度,优先让可运行的优先级高线程运行,但也只是概率提高,但使用cpu的线程会一直运行,直至完成
这时我们可以看到,cpu成了香饽饽,各位都蓄势待发的争抢,但有人拿到入场券后又会等待,直到有人使用出来,这时可能他还会回首掏(优先让可运行的优先级高线程运行,但也只是概率提高),大大增加再抢到cpu的概率
Java使用的是抢占式调度
3.同步与异步
同步:当(甲)某线程请求某个资源时,若(乙)另一线程正在使用该资源,甲会等到乙线程使用完毕再使用
异步:当(甲)某线程请求某个资源时,若(乙)另一线程正在使用该资源,甲也能申请到该资源,不必等待
对于多线程,同步特化安全方面,异步特化效率方面,两者并没有优劣,具体从要解决的问题分析。这里讲的同步强调数据(资源)共享要同步
异步容易造成的数据问题
可以看出,线程甲在对链表A遍历,而线程乙想要操作C元素,如果两线程没加安全锁,是异步模式,甲已然遍历该链表,后乙修改C元素,那甲所储存的数据与实际数据就发生了错误,如果集合还未不安全的结构,进程直接报错,程序崩溃
4. 并发与并行
并发:指 2~n 个事件在某时间段发生
并行:指 2~n 个事件在同一时刻发生
这其实是最好理解的一组概念了,可以将自己的手指想象成一个cpu,一个手指打LOL时无论运行(手速)多快,都只能一次执行一个事件,但又因为手速很快,看起来像同时执行了几个事件,这就是并发,而五个手指操作qwerf等键就是完全的并行,每个手指(cpu)在团战时都有各自的任务
2.如何使用代码开启线程
1. Thread (类)
public class ThreadTest {
public static void main(String[] args) {
Shopper shopper = new Shopper();
shopper.start();
//或者
//Thread thread = new Shopper();
//thread.start();
}
}
class Shopper extends Thread{
String need = "汉堡";
@Override
public void run() {
System.out.println("我想买个" + need);
}
}
通过新建线程继承抽象类Thread,需要重写run()方法,且run()方法就是线程要执行的任务,run()里的代码是一条新的执行路径。
该线程的触发方式为:thread对象的start()来启动
2. 实现Runnable(接口)
public static void main(String[] args) {
Runnable myRunnable = new Runnable() {
String need = "油条";
@Override
public void run() {
System.out.println("我想买个" + need);
}
};
new Thread(myRunnable).start();
}
或者
public static void main(String[] args) {
Runnable m = new myRunnable();
new Thread(m).start();
}
static class myRunnable implements Runnable{
String need = "油条";
@Override
public void run() {
System.out.println("我想买个" + need);
}
}
步骤:1.创建一个任务对象
2.创建一个线程,并分配一个任务
3.执行这个线程
3.两种线程创建并使用的方法总结
Runnable 与 Thread 相比
优势:
-
通过创建任务给线程分配来创建线程,更适合多个线程执行相同任务的情况(一核遇难,八核救援)
-
可以避免单继承带来的局限性(可以使一个线程完成更多任务(负载提升))
-
任务与线程分离,提高了程序的健壮性(适于逻辑思考)
-
线程池技术,仅接受Runnable类型的任务。
4.具有返回值的线程创建Callable(使用少)
public static void main(String[] args) {
Callable<String> c = new callable();
FutureTask<String> f = new FutureTask<>(c);
new Thread(f).start();
}
}
class callable implements Callable<String> {
String need = "面包";
@Override
public String call() throws Exception {
System.out.println(need);
return need;
}
不常用,了解即可。
3. Thread类常用方法分析
1. 构造方法
Thread有8种构造方法,通过上面的线程创建将其分类:
-
Thread继承创建
-
-
Thread()
-
分配一个新的
Thread
对象。
-
-
-
Thread(String name)
//给线程取名 -
分配一个新的
Thread
对象。
-
-
-
Thread(ThreadGroup group, String name)
-
//给线程分组,取名
-
分配一个新的
Thread
对象。
-
线程组(Thread Group):表示线程的集合,以树的方式存储,每个线程都有父线程组。
-
Runnable实现任务创建
-
-
Thread(Runnable target)
-
分配一个新的
Thread
对象。
-
-
-
Thread(Runnable target, String name)
//取名 -
分配一个新的
Thread
对象。
-
-
-
Thread(ThreadGroup group, Runnable target)
//分组 -
分配一个新的
Thread
对象。
-
-
-
Thread(ThreadGroup group, Runnable target, String name)
//取名并分组 -
分配一个新的
Thread
对象,使其具有target
作为其运行对象,具有指定的name
作为其名称,属于group
引用的线程组。
-
-
-
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
//加了个堆栈大小 -
分配一个新的
Thread
对象,以便它具有target
作为其运行对象,将指定的name
正如其名,以及属于该线程组由称作group
,并具有指定的 堆栈大小 。
-
2.设置和获取
观测指标:
设置于获取:标识符(Id),名称(Name),优先级(Priority),状态(State)
方法:
-
set+设置于获取观测指标();
-
get+设置于获取观测指标();
toString(); 方法:输出线程的名称,优先级,线程组等信息
设置与检测:线程中断,守护线程
线程中断:
interrupt();让其中断
interrupted();/isInterrupted(); 测试是否中断
守护线程:守护用户线程(默认),当最后一个用户线程结束时,自动全部死亡。
setDaemon(true); 设为守护线程
isDaemon(); 检测这个线程是否为守护线程
3. sleep()
-
-
sleep(long millis)
-
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
-
-
-
sleep(long millis, int nanos)
-
导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。
-
4.线程的尊严
线程应该被得到尊重,有自己中断自己的权利! (大雾)
==》其实就是实时监测,达到要求自动掐死。(具体检测环节写在任务的run方法中)
Thread对象.interrupt();给线程添加中断标记,只是给线程一个异常,我们(黑心老板)决定后续操作
5.线程的6种状态
6.线程安全
当我们使用多线程来使cpu干活时,经常会因为cpu时间片的分配导致出现逻辑错误,比如下面这个程序,会出现餐厅卖空了仍继续卖的情况。
public class Test {
public static void main(String[] args) {
Runnable sell = new sell();
new Thread(sell,"甲").start();
new Thread(sell,"乙").start();
new Thread(sell,"丙").start();
}
}
class sell implements Runnable{
private int hamburger = 10;
@Override
public void run() {
while (true){
if(hamburger > 0){
System.out.println("准备食材中");
hamburger--;
System.out.println(Thread.currentThread().getName()+"售卖了一个汉堡" + "剩余汉堡为" + hamburger);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
准备食材中
准备食材中
准备食材中
乙售卖了一个汉堡剩余汉堡为7
丙售卖了一个汉堡剩余汉堡为8
甲售卖了一个汉堡剩余汉堡为9
准备食材中
甲售卖了一个汉堡剩余汉堡为6
准备食材中
丙售卖了一个汉堡剩余汉堡为5
准备食材中
乙售卖了一个汉堡剩余汉堡为4
准备食材中
准备食材中
准备食材中
丙售卖了一个汉堡剩余汉堡为2
甲售卖了一个汉堡剩余汉堡为3
乙售卖了一个汉堡剩余汉堡为1
准备食材中
准备食材中
准备食材中
乙售卖了一个汉堡剩余汉堡为-2
甲售卖了一个汉堡剩余汉堡为-1
丙售卖了一个汉堡剩余汉堡为0
要解决这样的线程安全问题,有三种方法:
1.同步代码块
原理:通过synchronized(锁对象(Object))来对线程run任务种一段代码块打标记来锁住,当运行时不会被其他线程截胡。
class sell implements Runnable{
private int hamburger = 10;
Object o = new Object();
@Override
public void run() {
while (true){
synchronized (o) { //锁
if (hamburger > 0) {
System.out.println("准备食材中");
hamburger--;
System.out.println(Thread.currentThread().getName() + "售卖了一个汉堡" + "剩余汉堡为" + hamburger);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
准备食材中 甲售卖了一个汉堡剩余汉堡为9 准备食材中 甲售卖了一个汉堡剩余汉堡为8 准备食材中 甲售卖了一个汉堡剩余汉堡为7 准备食材中 甲售卖了一个汉堡剩余汉堡为6 准备食材中 甲售卖了一个汉堡剩余汉堡为5 准备食材中 甲售卖了一个汉堡剩余汉堡为4 准备食材中 甲售卖了一个汉堡剩余汉堡为3 准备食材中 丙售卖了一个汉堡剩余汉堡为2 准备食材中 丙售卖了一个汉堡剩余汉堡为1 准备食材中 丙售卖了一个汉堡剩余汉堡为0
这种方法有很大的缺陷,大部分时间其余的线程会阻塞,浪费内存,而且同一线程会对锁对象回首掏导致看起来就是单线程,所以可以优化为在汉堡数量告急时再使用锁来保证不出现逻辑错误。
2. 同步方法
将实际的运作代码块抽成一个方法,加入synchronized关键词修饰,再放入run();中方便使用
与第一个方法没有原理上的区别,在此不做演示。
3.显示锁Lock
原理: 建立锁对象:Lock lock = new ReentrantLock();
while(true){
lock.lock();
操作;
lock.unlock();
}
public class LockTest {
public static void main(String[] args) {
Runnable sell = new sell();
new Thread(sell,"甲").start();
new Thread(sell,"乙").start();
new Thread(sell,"丙").start();
}
static class sell implements Runnable{
Lock lock = new ReentrantLock();
private int hamburger = 10;
Object o = new Object();
@Override
public void run() {
while (true){
lock.lock(); //Lock显示锁
if (hamburger > 0) {
System.out.println("准备食材中");
hamburger--;
System.out.println(Thread.currentThread().getName() + "售卖了一个汉堡" + "剩余汉堡为" + hamburger);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
}
}
}
}
7.锁
线程死锁
线程死锁是跟锁有关的概念,当我们给各种方法套上锁,当两个线程需要互相调用被同一个锁修饰的方法时,就会产生线程死锁
public class ThreadDeadlock {
public static void main(String[] args) {
Study study = new Study();
Rest rest = new Rest();
Runnable play = new Runnable() {
@Override
public void run() {
try {
study.say(rest);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable learn = new Runnable() {
@Override
public void run() {
try {
rest.say(study);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(learn).start();
new Thread(play).start();
}
static class Study {
public synchronized void say(Rest r) throws InterruptedException {
System.out.println("我学累了就去休息.");
Thread.sleep(1000);
r.go();
}
public synchronized void go(){
System.out.println("我去学习了!");
}
}
static class Rest {
public synchronized void say(Study s) throws InterruptedException {
System.out.println("我休息好了就去学习.");
Thread.sleep(1000);
s.go();
}
public synchronized void go(){
System.out.println("我去休息了!");
}
}
}
结果为:
我休息好了就去学习. 我学累了就去休息.
产生死锁
8.多线程交互问题
经典的多线程交互问题为两个线程交替进行时,这时候不仅需要安全锁,同时为了防止回首掏还要进行标记来使线程交替运作,如下:
public class MultiThreadedInteractionProblem {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
public static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public void run() {
boolean flag =true;
for (int i = 0; i < 10; i++) {
if(flag){
try {
f.setNameAndTaste("蒙德土豆饼","美味的");
flag = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(!flag){
try {
f.setNameAndTaste("爪爪土豆饼", "奇怪的");
flag = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
f.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Food{
private String name;
private String taste;
//这里的flag是为了防止回首掏导致一个线程一直运行
private boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste) throws InterruptedException {
this.name = name;
if(flag){
Thread.sleep(100);
this.taste = taste;
flag = false; //条件true才会使其运行
this.notifyAll();
this.wait();
}
}
public synchronized void get() throws InterruptedException {
if(!flag){ //条件false才会使其运行
System.out.println("服务员端上的菜为:" + taste+name);
flag = true;
this.notifyAll();
this.wait();
}
}
}
}
服务员端上的菜为:美味的蒙德土豆饼 服务员端上的菜为:奇怪的爪爪土豆饼 服务员端上的菜为:美味的蒙德土豆饼 服务员端上的菜为:奇怪的爪爪土豆饼 服务员端上的菜为:美味的蒙德土豆饼 服务员端上的菜为:奇怪的爪爪土豆饼 服务员端上的菜为:美味的蒙德土豆饼 服务员端上的菜为:奇怪的爪爪土豆饼 服务员端上的菜为:美味的蒙德土豆饼 服务员端上的菜为:奇怪的爪爪土豆饼