Java多线程
线程和进程
线程是指程序在运行过程中,一个执行程序代码的执行单元,一个线程指的是进程中的一个单一独立的控制流,一个进程可以同时并发运行多个线程,并且每一个线程并行执行不同的任务。
进程是指一个正在执行的程序,一个进程包括由操作系统分配的内存空间,包含一个或多个线程,且一个线程不能独立存在,它必须是进程的一部分,一个进程一直运行直到所有的非守护线程都结束后才能结束进程,而多线程则能够达到编写高效率程序来充分的利用CPU资源的目的。在Java语言中线程共有五种状态,分别为新建、就绪、运行、挂起、死亡。
线程的机制
线程不是Java语言中独有的概念,这个名词来源于操作系统,操作系统使用分时管理进程,按照时间片轮转执行每个进程。一个进程包含有一个或多个线程,在程序运行时,由于资源和时间的分配都是有限的,此时遵守线程机制可以有效保证程序的正常和高效运转。线程机制包括线程的生命周期、线程的优先级、线程的操作和线程的安全。
Java多线程实现的方式
Java虚拟机允许应用程序并发的运行多个线程,在Java语言中多线程的实现方式一般有以下三种方式。
1. 继承Thread类,重写run()方法 |
---|
2. 实现Runnable接口,并实现该接口内的run() 方法 |
3. 实现Callable接口,并实现该接口内的call() 方法 |
继承Thread类
Thread类是Java.lang包下的一个实现多线程机制的Java类,Thread本质上也是实现了Runnable接口的实例,它代表一个线程的实例,并且启动线程的唯一方法就是同过Thread类下的start()方法,start()是一个native(本地方法),底层由其它语言实现,它将启动一个新线程,并且执行run方法,这种通过自定义类直接继承Thread类并重写run方法就可以开启新线程,并执行自定义的run方法。
注:调用start()方法之后并不是立即执行多线程代码,而是使该线程变为一个可运行状态,什么时候运行多线程代码是由操作系统决定的。
代码示例:
/**
* 继承Thread类
*/
public class Thread_01 extends Thread {
@Override
public void run() {
for(int i = 0; i <= 3; i++) {
System.out.println(Thread.currentThread() + ":" + i); //获取当前对象的引用
}
}
public static void main(String[] args) {
Thread_01 th1 = new Thread_01(); //创建对象
Thread_01 th2 = new Thread_01();
th1.start(); //开启th1线程
th2.start(); //开启th2线程
}
}
以上运行结果每一次都不一样,这是因为多线程的并发和时间片轮转导致,两个线程轮流抢占进程资源。
实现Runnable接口
- 自定义一个类并实现Runnable接口然后实现该接口方法
- 创建Thread对象,利用实现Runnable接口的对象作为参数实例化Thread对象
- 调用Thread对象的start方法开启线程
/**
* 实现Runnable接口
*/
public class Runnable_01 implements Runnable{
@Override
public void run() {
//循环遍历两次
for(int i = 0; i <= 2; i ++) {
//打印当前线程和线程的名称
System.out.println(Thread.currentThread().getName() + " " + "==>" + "implement runnable interface");
}
System.out.println(Thread.currentThread()); //打印当前线程
}
public static void main(String[] args) {
Runnable_01 t = new Runnable_01(); //创建自定义类的对象
Thread th1 = new Thread(t,"线程1"); //创建Thread线程对象
Thread th2 = new Thread(t,"线程2");
Thread th3 = new Thread(t,"线程3");
th1.start(); //开启th1线程
th2.start();
th3.start();
}
}
运行结果由三个线程轮流抢占资源,并在循环外面又打印了三个线程的名称。
Thread类和Runnable接口最终都是通过Thread对象的API来控制线程的。
实现Callable接口,重写call()方法
Callable接口属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,提供了比Runnable接口更强大的功能,主要有以下三个方面。
- Callable可以在任务结束后提供一个返回值,Runnable接口无法提供这个功能。
- Callable中的call()方法可以抛出异常,而Runnable接口不能抛出异常。
- 运行Callable可以得到一个Futrue对象,Futrue对象表示异步计算的结果,它提供了检查计算是否完成的方法,由于线程属于异步计算模型,因此无法从其他线程中得到函数的返回值,此时可以用Futrue对象监视目标线程调用call()方法的情况,当调用Futrue的get()方法获取结果时当前线程会进入阻塞状态,知道call()方法结束返回结果。
示例
/**
* 实现Callable接口
*/
public class Callable_01 implements Callable<String> {
public String call() throws Exception {
for(int i =0; i <= 5; i++) {
//打印当前线程和名称
System.out.println(Thread.currentThread().getName() + "------------");
}
return Thread.currentThread().getName() + "号线程"; //返回当前线程名称的结果
}
public static void main(String[] args) throws Exception {
//创建Callable对象
Callable<String> c1 = new Callable_01();
Callable<String> c2 = new Callable_01();
//创建FutureTask 对象传入Callable 参数
FutureTask<String> fu1 = new FutureTask<>(c1);
FutureTask<String> fu2 = new FutureTask<>(c2);
//执行Callable对象的方式,需要FutureTask对象的支持,用于接收运算结果
Thread th1 = new Thread(fu1);
Thread th2 = new Thread(fu2);
//开启两个线程
th1.start();
th2.start();
//打印两个线程的返回结果
System.out.println(fu1.get());
System.out.println(fu2.get());
}
}
以上三种创建线程的方式中,继承Thread和实现Runnable的方式都没有返回值,只有实现Callable方式才有返回值,当需要实现多线程时,一般推荐使用Runnable接口的方式,因为Thread类中定义的多种方法会被派生类重写而造成浪费。
线程的生命周期
当线程被创建被启动之后并不是立即就进入了执行的状态,也不是一直处于执行的状态,在线程的生命周期中,它要经过新建、就绪、运行、阻塞、死亡、锁定、等待、七种运作状态,当线程启动后也不是一直占着CPU独立运行,所以CPU需要在多个线程间切换,于是线程状态也会多次在运行、阻塞之间切换。
线程的七种状态
- 新建状态
当在类中使用了new关键字创建了一个线程后,该线程就处于新建的状态,此时由Java虚拟机为其分配内存并初始化成员变量的值。例如
Thread th = new Thread(); //创建一个线程对象
- 就绪状态
当线程对象调用了start()方法之后该线程就处于就绪状态,此时Java虚拟机会为这个对象创建方法调用栈和程序计数器,等待调度运行
- 运行状态
在线程执行run()方法的时候,该线程就处于运行的状态
- 阻塞状态
当处于运行状态的线程失去所占用的资源后则会进入阻塞状态,阻塞状态又有三种情况。
-
等待阻塞:通过调用线程的wait()方法,让线程等待某个线程的通过。
-
同不阻塞:线程在获取synchronized同步锁失败(锁被其它线程占用)后会进入阻塞状态.
-
其它阻塞:通过调用线程的sleep()、join或发出I/O请求时,线程会进入阻塞状态,当sleep()方法状态超时或者join()方法等待线程终止超时、I/O处理完毕时,线程重新转入就绪状态。
-
死亡状态
当run()方法执行完成或出现异常时,线程就会进入死亡并释放所有的资源。
- 锁定状态
锁定状态是指被synchronized块阻塞,即与线程同步有关的一个状态。
- 等待状态
Thread类的一个wait()方法能让当前线程进入等待状态,在等待过程中会让当前线程释放它持有的锁,直到其它线程调用此对象的notify()方法或者notifyAll()方法。
线程的优先级
在程序中,当有多个线程处于可运行状态时,运行系统会首选一个优先级最高的线程执行,只有当线程停止、推出或者由某些原因不执行时,低优先级的线程才会被执行,如果当两个优先级相同的线程同时等待执行时,运行系统会以round-robin 的方式选择一个线程执行。
在Java多线程当中为了使某些线程可以优先得到CPU资源,可以对线程设置优先级等级,设置的方法时setPriority(),线程的优先级分为1~10个等级,如果小于1或者大于10则抛出异常,默认值为5。
示例:
public class Thread_01 extends Thread {
//创建Thread_01类的构造方法
public Thread_01(String name) {
super(name); //继承父类构造方法
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName()); //调用getName()方法获取线程名称
}
}
public static void main(String[] args) {
Thread th1 = new Thread_01("Thread1"); //创建对象
Thread th2 = new Thread_01("Thread2"); //创建对象
th1.setPriority(6); //设置优先级的值为6
th2.setPriority(4); //设置优先级的值为4
th1.start(); //开启th1线程
th2.start(); //开启th2线程
}
在设置了优先级之后th2对象依然执行在最前面,因为通过线程的优先级无法完全保证按照优先级顺序来执行,但是设置优先级高的线程会更容易获得CPU资源,概率比优先级低的线程要大很多,优先级低的线程并不是没有执行在最前面的机会。
操作线程的方法
Java多线程中常见的操作方法有:线程名称修改、线程睡眠、线程同步、线程等待、线程死锁等方法…
修改线程名称
所有的线程程序的每一次执行都会有不同的运行过结果,因为线程会根据根据自身的情况进行资源的抢占,如果要区分每一个线程就必须依靠线程的名称,线程的名称一般会在其线程启动之前进行自定义,不建议在线程启动之后再做名称修改或是为不同的线程设置重复相同的名称。
示例:新建一个线程,将这个线程进行名称修改。
public class Thread_01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
System.out.println(Thread.currentThread().getName()); //获取当前线程和名称
}
}
public static void main(String[] args) {
Thread_01 th = new Thread_01();
Thread th1 = new Thread(th); //创建th1线程对象
th1.setName("旧线程名");
th1.start(); //开启th1线程
Thread th2 = new Thread(th); //创建th2线程对象
th2.setName("新线程名"); //修改线程名称
th2.start();
}
}
线程睡眠
线程睡眠的主要原因是线程设置了优先级,有可能出现优先级高的线程没有处理完成,优先级低的线程不容易得到运行,但在当优先级高的线程需要使用优先级低的线程配合处理程序时,优先级高的线程应该让出CPU资源,通常的做法是让线程进入睡眠状态。Thread类的sleep()方法的作用就是让当前线程睡眠,即当前线程会从运行状态进入睡眠(阻塞)状态,sleep()会指定睡眠时间,线程睡眠的时会间大于或等于该睡眠时间,在线程重新被唤醒时则会由阻塞状态变为就绪状态,从而等待CPU的调度。
sleep()具体有以下两种具体的实现方法
- sleep(long millis): 指定线程睡眠的毫秒数,long类型
- sleep(long millis, int nanos):指定线程睡眠的毫秒+纳秒数
示例: 继承Thread类创建一个线程,每个线程打印5次,分别等待50毫秒和60纳秒
public class Thread_01 extends Thread {
@Override
public void run() {
//循环五次线程信息
for (int i = 0; i < 5; i++) {
System.out.println("thread第" + (i + 1) + "次打印");
try {
Thread.sleep(50); //让线程睡眠阻塞50ms
//Thread.sleep(50,60); //让线程睡眠阻塞50ms,40ns
} catch (InterruptedException e) {
e.printStackTrace(); //打印异常信息
}
}
}
public static void main(String[] args) {
Thread_01 th1 = new Thread_01(); //创建自定义类对象
th1.start(); //开启线程
}
}
以上线程打印的信息会有延迟的效果,这是因为指定了线程睡眠的时间。
线程同步
同步当多个控制线程共享相同数据的内存时,需要确保每个线程访问到一致的数据。如果每个线程使用的变量都是其它线程不会读取或修改的,那么就不会存在线程一致性问题。当某个线程可以修改变量,而其它线程也可以读取或修改这个变量时,就需要对这些线程进行同步的操作,以确保它们再访问变量的存储内容时不会访问到无效的值。Java使用synchronized来处理线程同步的问题,由于Java的每个对象都有一个内置的锁,所以当此关键字修饰方法时,内置锁就会来保护整个方法,然后在调用该方法前需要先获得内置锁,否则就处于阻塞状态。synchronized实现同步的机制是指synchronized依靠锁机制来进行多线程同步的,锁有两种:一种是对象锁,一种是类锁。
依靠对象锁来锁定
在初始化一个对象时,自动有一个对象锁。synchronized方法锁依靠对象锁工作,多线程访问synchronized()方法,如果进入某个进程抢到锁之后,其它进程只能排队等待。
synchronized方法锁语法通常如下
synchronized void method(){}
#两种方式都一样
void method() {
synchronized(this){
}
}
示例
public class MySync {
public synchronized static void method1() throws InterruptedException {
System.out.println("线程1开始的时间:" + System.currentTimeMillis()); //打印当前时间
Thread.sleep(5000);
System.out.println("方法1开始的时间:" + System.currentTimeMillis());
}
public synchronized static void method2() throws InterruptedException {
//死循环
while (true) {
System.out.println("方法2正在执行");
Thread.sleep(200);
}
}
static MySync sync1 = new MySync(); //第一个实例对象
static MySync sync2 = new MySync(); //第二个实例对象
public static void main(String[] args) {
Thread th1 = new Thread(new Runnable() {
@Override
public void run() {
try {
sync1.method1(); //调用method1()方法
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 4; i++) {
try {
Thread.sleep(200); //睡眠阻塞200ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1还活着");
}
}
});
//使用lambda表达式写法
Thread th2 = new Thread(() -> {
try {
sync2.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
th1.start(); //开启线程th1
th2.start(); //开启线程th2
}
}
线程等待
Thread类中的wait()方法可以让当前线程进入等待状态,同时wait()方法也会让当前线程释放它所持有的锁,直到其它线程调用此对象的notify()方法活着notifyAll()方法唤醒当前对象上等待的线程,其中notify()是唤醒单个线程,而notifyAll()方法则唤醒所有线程。
示例
public class Wait extends Thread{
//创建Wait构造器,参数继承父类的属性
public Wait(String name) {
super(name);
}
@Override
public void run() {
synchronized (this) {
try {
Thread.sleep(1000); //使当前线程阻塞1秒,确保主程序的wait()方法执行后再执行notify()方法
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " call notify() ");
//唤醒当前的wait线程
this.notify();
}
}
//main方法
public static void main(String[] args) {
Wait thread = new Wait("A");
synchronized (thread) {
try {
System.out.println(Thread.currentThread().getName() + "开始线程");
thread.start(); //主线程等待thread 通过notify方法唤醒
System.out.println(Thread.currentThread().getName() + "wait()方法");
thread.wait();
//使当前执行wait()的线程等待
System.out.println(Thread.currentThread().getName() + "continue");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
线程死锁
死锁是指多个进程在运行过程中多个进程之间互相争抢资源造成的,当处于这种状态时如果不做其它操作则都无法继续运行下去,导致死锁的原因在于synchronized关键字的不恰当使用,synchronized关键字的作用是确保在某一刻只有一个线程被允许执行特定的代码块,因此被允许执行的线程首先必须拥有对变量或对象的访问权,当线程访问对象时,线程会给对象加锁,而这个锁会导致其它也想访问同一个对象的线程被阻塞,直到第一个线程释放它加在对象上的锁为止。
示例:多个锁之间的嵌套产生死锁
public class Lock implements Runnable{
private boolean flag;
public Lock(boolean flag) {
this.flag = flag;
}
@Override
public void run() { //重写run()方法
if (flag) {
while (true) { //死循环
synchronized (Lock.obj1) { //循环锁
System.out.println(Thread.currentThread().getName() + "....if...obj1");
synchronized (Lock.obj2) {
System.out.println(Thread.currentThread().getName() + "....if...obj2");
}
}
}
} else {
while (true) {
synchronized (Lock.obj2) {
System.out.println(Thread.currentThread().getName() + "....else...obj2");
synchronized (Lock.obj1) {
System.out.println(Thread.currentThread().getName() + "....else...obj1");
}
}
}
}
}
//创建两个静态Object对象
public static Object obj1 = new Object();
public static Object obj2 = new Object();
public static void main(String[] args) {
Lock lock1 = new Lock(true); //创建
Lock lock2 = new Lock(false);
Thread t1 = new Thread(lock1);
Thread t2 = new Thread(lock2);
t1.start();
t2.start();
}
}
以上运行结果产生死锁的原因是因为线程0想要得到obj2对象的锁然后进行下面的操作,而obj2被线程所占用,线程1想要得到obj1锁以进行下面的操作,而obj1对象的锁被线程0所占用。至此造成了线程之间的死锁堵塞。
线程安全
Java线程安全是在多线程编程时计算机程序中的一个概念,在拥有共享数据的多条线程并执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据错乱的情况。
一个线程是否需要是线程安全的,取决于该对象是否被多线程访问,这里指的是程序中访问对象的方式,而不是对象要实现的功能。要让对象是线程安全的就要采用同步机制来协同对对象可变状态的访问。
示例:创建一个线程不安全的类在多线程中使用。
public class Security implements Runnable{
//创建Count对象
Count count = new Count();
@Override
public void run() {
count.count();
}
public static void main(String[] args) {
Security security = new Security();
for (int i = 0; i < 5; i++) { //循环五个线程
//创建一个匿名线程类,并放人对象属性
new Thread(security).start();
}
}
}
//新建一个Count计数类
class Count {
private int sum;
public void count() {
for (int i = 0; i < 5; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + "-" + sum);
}
}
程序运行结果的不一致是由于存在成员变量的类用于多线程时时不安全的,不安全体现在这个成员变量可能发生非原子性操作,而变量定义在方法内,也就是局部变量,线程则是安全的。