进程与线程
进程是程序的一次动态执行过程,简单说类似应用程序的执行都是进程,QQ、酷狗
多进程的操作系统能通过是运行多个进程,由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片
由于CPU执行速度非常快,所以看起来所有程序好像是同时运行一样
多线程是实现并发机制的一种有效手段
进程和线程一样,都是实现并发的基本单位
线程是比进程更小的执行单位,一个进程中至少包含一个线程
所谓的多线程,指的是一个进程在执行过程中可以产生多个线程,这些线程可以同时存在,同时运行
传统的程序语言中,运行顺序总是按照程序的流程运行,且一次只能运行一个程序块
Java的多线程打破了这种束缚,多线程机制指的是同时运行多个程序块,使得程序运行的效率更高
形象化多进程与多线程
CPU相当于一个工厂,时刻在运行
车间相当于一个进程
工人相当于一个线程
一个进程可以包含多个线程
车间内的空间是工人们共享的,象征着一个进程的内存空间是共享的,每个线程都可以使用这些共享内存
但是每个房间的大小不一样,有的房间最多只能容纳一个人,比如厕所有人在的时候其他人就不能进去,这代表了一个线程使用某些共享内存时,其他线程必须等他结束才能使用这一块内存
有一个简单的方法防止其他人进入,就是在门口加一把锁,后门的人看到上锁,就会去排队,等到锁打开后再进去,这就是互斥锁,防止多个线程同时读写同一个内存区域
还有某些区域,可以同时容纳多个人,比如厨房,如果人数大于n,超出的人只能在外面等着,这代表了某些内存区域,只能供给固定数量的线程使用
这时的解决办法是,在门口挂多把钥匙,进去的人就取走一把钥匙,出来时把钥匙换回原处,后到的人发现钥匙架子空了,就必须在门口处排队,这种做法叫做信号量,用来保证多个线程不会互相冲突
总结
-
操作系统以多进程形式,允许多个任务同时运行;
-
以多线程的形式,允许单个任务分成不同的部分同时运行;
-
提供协调机制,一方面防止进程之间和线程之间产生冲突,一方面允许进程之间和线程之间共享资源;
Java中的线程实现
在Java中实现多线程有2种方式
1、 继承Thread类
2、 实现Runnable接口
方式一:继承Thread类
一个类只要继承Thread类,就被称为多线程操作类
在继承后,需要重写Thread类中的run方法,此方法为线程的主体
正确启动多线程,不是直接调用run方法,而是调用start方法
从程序的运行结果中可以看出,两个线程交错运行,即哪个线程对象抢到了CPU资源,哪个线程就会运行
所以每次的运行结果都是不一样的,在线程启动时虽然调用的是start方法,但实际上还是会调用run方法运行线程主体
public class TestThread extends Thread {
private String name;
private int num;
public TestThread(){
}
public TestThread(String name){
this.name = name;
}
@Override
public void run() {
for (int num = 0; num <= 20; num++) {
System.out.println(name + num);
}
}
}
public class TestThread2 {
public static void main(String[] args) {
TestThread thread1 = new TestThread("A");
TestThread thread2 = new TestThread("B");
thread1.start();
thread2.start();
}
}
为啥不能直接使用run方法,而是调用start方法
因为多线程运行需要依赖于操作系统,以下是Thread类中start()方法源码
public synchronized void start() {
// 多次调用同一个线程的start方法时会抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
// 线程启动真正调用的是start0()方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// native表示调用本机操作系统的函数
private native void start0();
所以,继承Thread类实现多线程时注意同一个线程不要多次调用start()方法,会抛出异常
Java中存在单继承的限制,所以还有另一种方式实现多线程
方式二:实现Runnable接口
通过实现Runnable接口,重写其中的run方法实现多线程
方式一继承Threa类时想要启动一个线程采用start()方法,而Runnable接口中没有start()方法
此时还是需要Thread类完成启动,通过Thread类的2个构造方法,接收Runnable的实例对象,再调用start()方法启动多线程
public Thread(Runnable target)
public Thread(Runnable target, String name)
public class TestThread2 implements Runnable {
private String name;
private int num;
public TestThread2(){
}
public TestThread2(String name){
this.name = name;
}
public void run() {
for (int num = 0; num < 20; num++) {
System.out.println(name + num);
}
}
}
public class MainThread {
public static void main(String[] args) {
TestThread2 thread1 = new TestThread2("A");
TestThread2 thread2 = new TestThread2("B");
Thread t1 = new Thread(thread1, "A");
Thread t2 = new Thread(thread2, "B");
t1.start();
t2.start();
}
}
由此看,所有的多线程都依赖于Thread类的start()方法启动
Thread类与Runnable接口关联
通过Thread和Runnable接口都可以实现多线程
public class Thread extends Object implements Runnable
从Thread类的定义可以清楚发现,Thread类也是实现Runnable接口的,不过Thread类中的run方法是直接调用Runnable接口中的run方法的
Thread类与Runnable接口的关系类似于代理设计
实际上,使用Thread类和Runnable实现多线程也是有区别的
如果一个类继承Thread实现多线程,则不适合用于多个线程线程共享资源
通过实现Runnable接口实现多线程,可以方便的实现资源共享
public static void main(String[] args) {
// 线程共享,即多个线程共同执行一个任务
TestThread2 thread = new TestThread2("A");
new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
}
上面的代码是启动3个线程,只对一个任务进行处理,即==线程对象共享==
更建议使用实现Runnable接口的方式实现多线程
-
避免Java单继承
-
适合多个相同程序代码的线程去处理同一个任务
线程的状态
在主进程种创建多个线程对象,即可实现多线程
线程一般具有5种状态
1)创建状态 new Thread();
在程序中用构造方法创建一个线程对象后,新的线程对象便处于新建状态
此时,它已经有了相应的内存空间和其他资源,但还处于不可运行状态
新建线程,可使用Thread thread = new Thread();
2)就绪状态 start()
新建线程对象后,调用该线程的start()方法就可以启动线程
当线程启动时,先进入就绪状态,即线程进入线程队列排队,等待CPU服务
注意,此时表示具备运行条件,暂未开始运行
3)运行状态 run()
当就绪状态的线程被调用并获得CPU资源时,线程就进入了运行状态
此时,会调用该线程的run()方法,表明该线程开始执行
4)阻塞状态 sleep()、suspend()、wait()
一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作,会让出CPU资源,并中止执行,进入阻塞状态
在可执行状态下,如果调用sleep()、suspend()、wait()等方法时,线程进入阻塞状态
阻塞时,线程不能进入排队队列,只有当阻塞原因被消除后,线程才能进入就绪状态
5)终止状态 stop()
线程调用stop()方法或run()方法执行结束后,即处于死亡状态
处于死亡状态的线程不具有继续运行的能力
Thread类中常用的线程操作方法
虽然推荐用实现Runnable接口的方式实现多线程,但是多线程的方法都不在Runnable接口中,而是在Thread类中
1)public Thread(Runnable target) 构造方法 接收Runnable接口子类对象,实例化Thread对象
2)public Thread(Runnable target, String name) 构造方法 接收Runnable接口子类对象,实例化Thread对象,并设置线程名称
3)public Thread(String name) 构造方法 实例化Thread对象,并设置线程名称
4)public static Thread currendThread() 返回当前正在执行的线程
5)public final String getName() 返回线程的名称
6)public final int getPrority() 返回线程的优先级
7)public boolean isInterrupted() 判断目前的线程是否被中断,如果是返回true,否则返回false
8)public final boolean isAlive() 判断线程是否在活动,如果是返回true,否则返回false
9)public final void join() throws InterruptedException 等待线程终止
10)public final void join(long millis) throws InterruptedException 等待millis毫秒后,线程终止
11)public void run() 执行线程
12)public final void setName(String name) 设置线程名称
13)public final void setPriority(int newPriority) 设置线程的优先级
14)public static void sleep(long mills) throws InterruptedException 使得目前正在执行的线程休眠millis毫秒
15)public void start() 开始执行线程
16)pulic void toString() 返回代表线程的字符串
17)public static void yield() 将目前正在执行的线程暂停,允许其他线程执行
18)public final void setDeamon(boolean on) 将一个线程设置成后台运行
线程常用方法解析
1、 设置获取线程名称
public final String getName()
public final void setName(String name)
线程的名称一般在启动线程前设置,但是也允许为已经运行的线程设置名称
允许两个Thread对象有相同的名称,但是应该尽量避免
如果没有设置线程名,系统会自动为其分配名称,如Thread-0
获取当前线程的名称,Thread.currentThread().getName()
在Java中所有的线程都是同时启动的,哪个线程先抢占到CPU资源,哪个就会先运行
Java程序每次运行至少启动几个线程?
至少启动2个线程,我们知道Java命令执行程序时,实际上是启动一个JVM
JVM是在操作系统中启动一个进程,JVM会启动main主线程,另一个是GC垃圾回收线程
2、 判断线程是否启动
前面学过线程共有5个状态,创建 - 就绪 - 执行 - 阻塞 - 终止
当执行start()方法时,线程会通知CPU已经准备就绪,然后等待分配CPU资源
Java中使用isAlive()方法来判断线程是否已经启动
不过输出结果存在不确定性,因为线程可能存活有可能执行完了
注意,其他线程并不会受主线程的结束而结束
3、 线程的强制运行
可以使用join()方法让一个线程强制运行,强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行
4、 线程的休眠
在程序中允许一个线程进行暂时的休眠,直接使用Thread.sleep()方法即可实现休眠,一般用于延迟操作
注意单位为毫秒,1s = 1000ms
5、 中断线程
可以直接使用interrupt()方法中断其运行状态
6、 后台线程
直接使用setDaemon(true)方法,前台的主线程执行结束,但是后台线程依然会继续执行
线程的优先级
在Java的线程操作中,所有的线程在运行前都会保持在就绪状态
那么此时,哪个线程的优先级高,哪个线程就有可能会先被执行
Java中,线程的优先级使用setPriority()方法设置
Java中,一共有3个优先级
1)MIN_PRIORITY 最低优先级 常量表示为1
2)NORM_PRIORITY 中等优先级,线程的默认优先级,常量表示为5
3)MAX_PRIOTITY 最高优先级,常量表示为10
TestThread testThread1 = new TestThread("A");
TestThread testThread2 = new TestThread("B");
TestThread testThread3 = new TestThread("C");
Thread thread1 = new Thread(testThread1);
Thread thread2 = new Thread(testThread2);
Thread thread3 = new Thread(testThread3);
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.NORM_PRIORITY);
thread3.setPriority(10);
thread1.start();
thread2.start();
thread3.start();
注意,并非线程的优先级越高就一定会先执行,哪个线程限制性将由CPU的调度决定
主方法的线程优先级是NORM_PRIORITY,这个也是默认的优先级
System.out.println(Thread.currentThread().getPriority());
线程的礼让
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。
因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。
在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
同步锁和死锁
一个多线程的程序如果是通过Runnable接口实现的,则意味着类中的属性将被多个线程共享,这样就会造成一个问题
如果多线程要操作同一个资源,就有可能出现资源的同步问题,如卖票将一张票卖给两个人
public class TestThread extends Thread {
private String name;
private int ticket = 5;
public TestThread(){
}
public TestThread(String name){
this.name = name;
}
@Override
public void run() {
while (true) {
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + "还有余票" + (ticket--));
}
}
}
}
public static void main(String[] args) {
// 方式二:实现Runnabel,将其子类赋给Thread对象启动线程
// 只有这种方式才能对资源共享
TestThread2 t = new TestThread2();
Thread thread1 = new Thread(t,"A");
Thread thread2 = new Thread(t,"B");
Thread thread3 = new Thread(t,"C");
thread1.start();
thread2.start();
thread3.start();
}
结果
B还有余票10
C还有余票8
A还有余票9
C还有余票6
B还有余票7
B还有余票3
C还有余票4
C还有余票1
A还有余票5
B还有余票2
使用同步解决问题
从结果中可以发现,当多个线程同时操作一个共享数据时,存在一个线程未执行完,另一个线程插队执行的问题
此时需要同步,即多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行(类比排队上厕所)
1)synchronized同步代码块
synchronized(同步对象){
需要同步的代码
}
在使用同步代码块时必须指定一个需要同步的对象,但是一般都是将当前对象this设置成同步对象
public class TestThread2 implements Runnable {
private String name;
private int ticket = 10;
public TestThread2(){
}
public TestThread2(String name){
this.name = name;
}
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "还有余票" + (ticket--));
}
}
}
}
}
public static void main(String[] args) {
// 方式二:实现Runnabel,将其子类赋给Thread对象启动线程
// 只有这种方式才能对资源共享
TestThread2 t = new TestThread2();
Thread thread1 = new Thread(t,"A");
Thread thread2 = new Thread(t,"B");
Thread thread3 = new Thread(t,"C");
thread1.start();
thread2.start();
thread3.start();
}
结果为
A还有余票10
A还有余票9
A还有余票8
A还有余票7
A还有余票6
A还有余票5
A还有余票4
C还有余票3
C还有余票2
C还有余票1
2)synchronized同步方法
也可以使用synchronized关键字将一个方法声明为同步方法
public class TestThread2 implements Runnable {
private String name;
private int ticket = 10;
public TestThread2(){
}
public TestThread2(String name){
this.name = name;
}
public void run() {
while (true) {
sale();
}
}
public synchronized void sale(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "还有余票" + (ticket--));
}
}
}
public static void main(String[] args) {
// 方式二:实现Runnabel,将其子类赋给Thread对象启动线程
// 只有这种方式才能对资源共享
TestThread2 t = new TestThread2();
Thread thread1 = new Thread(t,"A");
Thread thread2 = new Thread(t,"B");
Thread thread3 = new Thread(t,"C");
thread1.start();
thread2.start();
thread3.start();
}
注意看,Java中方法前面的修饰符前后顺序
[访问权限符] final static synchronized [返回值类型] 方法名称(参数类型 参数名) throws 异常类 {方法体}
注意,上面的run()方法中的while会导致程序一直不会停止,改成for即可
死锁
一般多线程都是针对资源共享的业务,同步可以解决线程插队导致数据混乱的现象
但是过多的同步也会产生问题,比如A等B,B等C,C等A,结果造成死锁
死锁,即两个线程都在等待彼此完成,造成程序的停滞
注意,多线程共享资源时,一定要同步,但是过多的同步会造成死锁现象
目前多线程学的比较浅,不要太较真,继续学习,后面会进行专项学习
生产者和消费者
生产者生产出信息后,将其放到一个区域中
消费者从此区域中取出数据
但是存在2个问题
1)假设生产者线程刚往存储空间里添加了信息的名称,还没来得及放信息内容,程序就切换到消费者线程,结果消费者取到空数据
2)生产者放了若干次的数据,消费者才开始取数据
3)消费者取完一个数据,还没等生产者放入新的数据,就又重复取出重复数据