多线程简介
1.1并发和并行
- 并行:指两个或多个事件在同一时刻发生(同时发生)。多核CPU的基础上
- 并发:指两个或多个事件在同一个时间段内发生。单核CPU的基础上
在操作系统上,单CPU系统中,每一时刻只能运行一个程序,宏观是多个程序同时运行,微观是分时交替进行,是因为分时交替运行的时间非常短。
多核处理器可以每个处理器并发执行程序,这样多个程序可以同时执行,提高电脑运行效率。
单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也一样的,从宏观角度上理解线程是并行运行的,但是从微观上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会已某种顺序执行多个线程,我们把这种情况称之为线程调度。
1.2线程和进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程
- 线程:进程内部的一个独立执行单元;一个进程可以同时并发的运行你多个线程,可以理解为一个进程便相当于一个单CPU操作系统,而线程便是这个系统中运行的多个任务
线程与进程的区别: - 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程
- 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
注意:
- 因为一个进程中的多个线程是并发运行的,从微观上看也是有先后顺序的,哪个线程执行完全取决于CPU的调度,程序员干涉不了。这就造成了多线程的随机性。
- java程序的进程里至少包含两个线程,主进程也就是main()方法线程,另外一个是垃圾回收机制线程。每当使用java命令执行一个类时,实际都会启动一个JVM,每个JVM实际上就是在操作系统中启动了一个线程,java本身具备了垃圾的收集机制,所以在java运行时至少会启动两个线程。
- 由于创建一个线程的开销比创建一个进程小的多,我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
线程调度:
计算机通常只有一个CPU时,在任意时刻只能执行一条计算机指令,每个进程只有获得CPU的使用权才能执行指令。所谓多进程并发运行,从宏观上其实是各个进程轮流获得CPU的使用权,分别执行各自任务。那么在可运行池中,会有多个线程处于就绪状态等到CPU,JVM就负责了线程的调度。JVM采用的事抢占式调度,没有采用分时调度,因此可以造成多线程执行结果的随机性。
多线程实现的两种方式
1.1继承Thread类
构造方法:
- public Thread():分配一个新的线程对象
- public Thread(String name):分配一个指定名字的新的线程对象。
常用方法: - public String getName():获取当前线程的名称。
- setName 设置新的名字
- public void start():导致此线程开始执行;java虚拟机调用此线程的run方法。
- public void run():此线程要执行的任务在此处定义代码。
- public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂停停止执行)
- public static Thread currentThread():返回对当前正在执行的线程对象的引用。
class A extends Thread{
public void run(){
// 重写Thread类的run方法,run方法是编写线程任务的地方
for(int i=1;i<=30;i++){
System.out.println("A..."+i);
}
}
}
public class Test2 {
public static void main(String[] args) {
A a = new A();
/* a.run(); // 方法调用,不能实现并发的效果
*/
a.start(); // 启动线程A --> 线程启动后,自动会调用run方法
}
}
main也是线程(主线程)
1.2实现Runnable接口
只需要重写run方法即可
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的案列,并以此案例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
- public Thread(Runanble target):分配一个带有指定目标新的线程对象
- public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。
- 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
通过实现Runnable接口,使得该类有了多线程的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run()方法里面。Thread类实际也是实现了Runnable接口的类。
在启动的多线程的时候,需要通过Thread类的构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
1.3Thread和Runnable的区别
一个类继承Thread则不适合资源共享。如果是实现Runnable接口的话,很容易实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 线程池只能放入实现Runnable或callable类线程,不能直接放入继承Thread类。
扩充:在java中,每次程序运行至少启动两个线程。一个是main线程买一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实就是在操作系统启动了一个进程。
1.4匿名内部类方式实现线程的创建
使用线程的内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重写Runnable接口中的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
线程的生命周期
1.1生命周期概述
线程是一个动态的概念,有创建的时候,也有运行和变化的时候,也就有消亡的时候,所以从生到死就是一个生命周期。在生命周期中,有各种各样的状态,这些状态可以相互转换。
- 新建态:刚创建好对象的时候,刚new出来的时候。
- 就绪态:线程准备好了所有运行的资源,只差CPU来临。
- 运行态:CPU正在执行的线程的状态
- 阻塞态:线程线程主动休息、或者缺少一些运行的资源,及时CPU来临,也无法运行。
- 死亡态:线程运行完成、出现异常。调用方法结束
1.2java中关于线程状态的描述
java语言中,获取线程状态的方法:
getState():返回当前线程对象的状态对象;
说明:state的六种状态
- NEW:新建态,没有开启线程
- RUNNABLE:就绪态和运行态
- BLOCKED:阻塞态(等待锁、I\O)
- WAITING:阻塞态(调用了wait方法,等待其他线程唤醒的状态)
- TIMED_WAITING:阻塞态(调用了有时间限制的wait方法、sleep方法)
- TERMINATED:死亡态
1.3主线程
在任何java程序启动时,一个线程立刻运行(即main方法对应的线程),该线程通常称为程序 的主线程。
主线程的特点:
它是产生其他子线程的线程。
它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。
class A extends Thread{
public void run() {
for(int i=1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
try {
sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class B extends Thread{
public void run() {
for(int i=1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"<<<"+i);
try {
sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class Test2 {
public static void main(String[] args) {
new A().start();
new B().start();
for(int i=1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"<<<"+i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
多线程相关操作
1.1停止线程的方式
- 线程stop方法(过时)
- 定义标识的方法
public static boolean flag = false; //定义一个标识
1.2线程的优先级
每个线程都有优先级,优先级高的获得较多的执行机会。
只有在系统资源紧张的时候设置优先级才有意义,否则执行时机是一样的。
但是并不是有高优先级的线程执行时,低优先级的线程就不能执行。
Java中,线程优先级分为10级
- Thread.MAX_PRIORITY = 10
- Thread.NORM_PRIORITY = 5
- Thread.MIN_PRIORITY = 1
getPriority():返回当前线程对象的优先级,默认是5级
setPriority(int newPriority):设置线程的优先级,虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大10,最小1,默认5)
Thread.currentThread().setPriority(10); // 设置线程优先级
System.out.println(Thread.currentThread().getPriority()); //获取线程优先级
多线程相关操作
1.1线程的join方法
非静态方法 ,当前线程挂起(阻塞),等待加入(join)的线程执行完成。
当前线程用t.join()意味着,当前线程挂起直到目标线程t结束。
写在哪个线程里面哪个线程就会挂起,暂停执行,等到目标线程执行完毕,在执行当前线程。
if(i==10){
t1.start();
t1.join();
// 当主线程的i==10时,t1线程加入了
// 主线程挂起(暂停), 当t1线程运行结束后,主线程继续
1.2线程的yield方法
和sleep方法很像,但是和sleep不同,它们只是短暂的挂起当前线程,让别的线程先运行,进入到就绪状态。
。。。。
1.3守护线程(后台线程)
当所有线程完成时,java程序退出,这句话并不准确,因为gc这种垃圾回收线程,我们无法停止,但是java程序在所有非守护线程完成后退出,守护线程只有在别的线程运行的时候才会有意义。
后台线程的特征:当所有前台线程都运行完成后,无论后台线程是否运行完成都要结束。(是一段时间内结束,并不会理解结束)
实现:setDaemon(boolean on)
必须在启动线程之前(调用start方法之前)调用setDaemon(true)方法,才可以把该线程设置为后台线程。
Thread t1 = new Thread(new AA());
Thread t2 = new Thread(new BB());
t1.setDaemon(true);
// 把t1设置成守护线程(后台线程)
// 当t2结束后,t1也结束
t1.start();
t2.start();
多线程相关操作
1.1线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话可能影响线程安全。
1.2线程同步
当我们使用多个线程访问统一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
要解决多线程并发访问多个资源的安全性问题,java提供了同步机制(synchronized)来解决。
三种方式完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
1.3同步代码块
- **同步代码块:**synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问
- 格式:
synchronized(同步锁){
需要同步操作的代码}
- 同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
- 锁对象可以是任意类型
- 多个线程对象要使用同一把锁
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着。
1.4同步方法
- 同步方法:
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。 - 格式:
public synchronized void method(){
可能产生线程安全问题的代码
}
1.5lock锁
- java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有功能Lock都有,除此之外更强大,更体现面向对象。
- Lock锁也称同步锁,加锁与释放锁方法化了
-public void lock():加同步锁
public void unlock():释放同步锁
1.6死锁
- 多个线程竞争多个排它锁的时候,可能出现死锁
- 互相等待对方资源成为死锁
- 最典型的死锁:线程1持有ObjectA上的锁,并等待ObjectB上的锁,线程2持有ObjectB上的锁,并等待OBjectA上的锁,并一直等待第二个锁,因此会永远等下去。
public class Demo15 {
public static void main(String[] args) {
X x = new X();
Y y = new Y();
x.start();
y.start();
}
}
class X extends Thread{
public void run(){
synchronized ("aaa") {
System.out.println("线程X锁定aaa,并等待锁定bbb");
synchronized ("bbb") {
}
}
}
}
class Y extends Thread{
public void run(){
synchronized ("bbb"){
System.out.println("线程Y锁定bbb,并等待锁定aaa");
synchronized ("aaa") {
}
}
}
}