目录
第一部分:基本概念
要明白什么是线程,先要明白什么是进程。
- 进程:简单理解为正在进行中的程序
- 线程:就是进程中一个负责程序执行的控制单元(执行路径)
- 多线程:一个进程中有多个执行路径。(马路上有多车道)
开启多个线程是为了同时运行多部分代码(杀毒的同时清理垃圾)。
每一个线程都有自己运行的内容。这个内容可以称为线程要执行的任务。
多线程的好处:解决了多部分代码同时运行的问题。
多线程的弊端:线程太多 效率降低。(因为应用程序的执行都是cpu在做着快速的切换完成的。这个切换是随机的。)
JVM启动时就启动了多个线程,至少有两个线程可以分析的出来。
1,执行main函数的线程,该线程的任务代码都定义在main函数中。
2,finalize 负责垃圾回收的线程。
第二部分:如何创建一个线程
创建线程方式一:继承Thread类。
步骤:
1,定义一个类继承Thread类。
2,覆盖Thread类中的run方法。(run方法中定义就是线程要运行的任务代码。)
3,直接创建Thread的子类对象创建线程。
4,调用start方法(不是run方法!)开启线程并调用线程的任务run方法执行。
注释:
- 不是类中的所有代码都需要被线程执行。为了区分哪些代码能够被线程执行,java提供了Thread类的run()方法来包含那些被线程执行的代码。
- 调用run()和调用start()的区别:调用run()仅仅是普通的调用,是单线程的,而调用start()分为了两步,先启动了线程,再由jvm调用该线程的run()。
class Demo2 extends Thread //创建线程的第一步:继承Thread类
{
private String name;
Demo2(String name)
{
this.name = name;
}
public void run() //创建线程的第二步: 覆盖Thread类中的run方法:写入要执行的代码
{
//需要此线程完成的功能代码
System.out.println("名字是"+name);
}
}
class ThreadDemo2
{
public static void main(String[] args) {
Demo2 d1 = new Demo2("旺财"); //创建线程的第三步:创建Thread的子类对象创建线程。
Demo2 d2 = new Demo2("xiaoqiang");
d1.start();//创建线程的第四步:调用start方法开启线程并调用线程的任务run方法执行,虚拟机会调用该线程的run方法
d2.start();
System.out.println("over...."+Thread.currentThread().getName()); //获取线程的名称,格式:Thread-编号(从0开始)
}
}
(补充:currentThread():返回当前正在执行的线程对象)
三种运行结果:(多线程理解为不同车道,不同车道实际车速不同,故运行结果会有偏差)
创建线程方式二:实现Runnable接口。
1,定义类实现Runnable接口。
2,覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3,通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。
所以要在线程对象创建时就必须明确要运行的任务。
4,调用线程对象的start方法开启线程。
class Demo3 implements Runnable //创建线程的第一步:定义类实现Runnable接口。
{
public void run()//创建线程的第二步:覆盖接口中的run方法,将线程的任务代码封装到run方法中。
{
for(int x=0; x<20; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
class ThreadDemo3
{
public static void main(String[] args)
{
Demo3 d = new Demo3();
Thread t1 = new Thread(d);//创建线程的第三步:通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
Thread t2 = new Thread(d);
t1.start();//创建线程的第四步:调用线程对象的start方法开启线程。
t2.start();
}
}
实现Runnable接口的好处:
1,将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将线程任务的封装成对象。
2,避免了java单继承的局限性。
所以,创建线程的第二种方式较为常用。
例子:四个窗口一起共卖100张票(有待改进版)
class Ticket implements Runnable
{
private int num = 100;
public void run()
{
while(true)
{
if(num>0)
{
System.out.println(Thread.currentThread().getName()+" is saleing 第"+num--+" 张票");
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();//创建一个线程任务对象。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
// t1.start();
// t1.start();//一个线程不能开启两次 (线程结束后也不能再次开启),会抛出无效线程状态异常 illegalThreadStateException
}
}
运行结果(截取部分,其实是按顺序卖的,只是卖掉了来不及打印,所以打印出来是乱序):
线程的状态图:
设置线程的优先级:
第三部分:线程的安全问题
例子:
在售票过程中,非理想情况下,当多个线程同时进入判断语句(假设此时num=1,线程认为符合条件,进入),如下图。线程需要排队等待处理。执行线程0后,num=0,再执行线程1后,num=0,(此时已不对,不能售出第0张票)再执行线程2与线程3,就会造成不对的执行后果。
不正确的后果:
线程安全问题产生的原因:
1,多个线程在操作共享的数据。
2,操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。
就会导致线程安全问题的产生。
线程安全问题解决思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,
其他线程时不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。 (简单的说就是不让线程同时做同一件事,得排队)
在java中,用同步代码块(对多条语句的封装)就可以解决这个问题。
同步代码块的格式:
synchronized(对象)
{
需要被同步的代码 ;
}
同步函数:可视作同步代码块的简写形式,将synchronized关键字放入函数即为同步函数
同步函数和同步代码块的区别:
同步函数的锁是固定的this,谁调用谁是锁
同步代码块的锁是任意的对象。
静态的同步函数使用的锁是 该函数所属字节码文件对象
可以用 getClass方法获取,也可以用当前 类名.class 表示。
售票例子改进版:
class Ticket implements Runnable
{
private int num = 100;
Object object =new Object();
public void run()
{
while(true)
{
synchronized(object)
{
if(num>0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+" is saleing 第"+num--+" 张票");
}
}
}
}
}
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。可能会发生死锁
同步的前提:同步中必须有多个线程并使用同一个锁。
单例模式涉及的多线程问题
//懒汉式
class Single{
private static Single single =null;
private Single() {}
public static Single getInstance() {
if(single==null) //加入双重判断是为了解决效率问题。
//具体流程为:线程们判断single是否为空,先判断完成为空的线程进同步代码块,则其他的线程无法进去,
//先进去的执行完出来以后,后面的进程进去再进行判断,发现不为空,则不用再new
{
synchronized (Single.class) //加入同步为了解决多线程安全问题。
{
if(single==null)
single=new Single();
}
}
return single;
}
}
学习多线程基础,这一篇就够啦!(二):https://blog.csdn.net/weixin_43827227/article/details/96982701
多线程总结:
1,进程和线程的概念。
|--进程:
|--线程:
2,jvm中的多线程体现。
|--主线程,垃圾回收线程,自定义线程。以及他们运行的代码的位置。
3,什么时候使用多线程,多线程的好处是什么?创建线程的目的?
|--当需要多部分代码同时执行的时候,可以使用。
4,创建线程的两种方式。★★★★★
|--继承Thread
|--步骤
|--实现Runnable
|--步骤
|--两种方式的区别?
5,线程的5种状态。
对于执行资格和执行权在状态中的具体特点。
|--被创建:
|--运行:
|--冻结:
|--临时阻塞:
|--消亡:
6,线程的安全问题。★★★★★
|--安全问题的原因:
|--解决的思想:
|--解决的体现:synchronized
|--同步的前提:但是加上同步还出现安全问题,就需要用前提来思考。
|--同步的两种表现方法和区别:
|--同步的好处和弊端:
|--单例的懒汉式。
|--死锁。
7,线程间的通信。等待/唤醒机制。
|--概念:多个线程,不同任务,处理同一资源。
|--等待唤醒机制。使用了锁上的 wait notify notifyAll. ★★★★★
|--生产者/消费者的问题。并多生产和多消费的问题。 while判断标记。用notifyAll唤醒对方。 ★★★★★
|--JDK1.5以后出现了更好的方案,★★★
Lock接口替代了synchronized
Condition接口替代了Object中的监视方法,并将监视器方法封装成了Condition
和以前不同的是,以前一个锁上只能有一组监视器方法。现在,一个Lock锁上可以多组监视器方法对象。
可以实现一组负责生产者,一组负责消费者。
|--wait和sleep的区别。★★★★★
8,停止线程的方式。
|--原理:
|--表现:--中断。
9,线程常见的一些方法。
|--setDaemon()守护线程
|--join();
|--优先级
|--yield();
|--在开发时,可以使用匿名内部类来完成局部的路径开辟。