JAVA多线程
进程和线程
进程
是指运行中的应用程序,
一个应用程序可以同时启动多个进程。
进程是系统中独立存在的实体,拥有自己独立的资源,拥有自己私有的地址空间。
进程是系统进行资源分配和调度的一个独立单位。
线程
是程序执行流的最小单元。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位
线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
线程和进程的主要区别在于:
每个进程都需要操作系统为其分配独立的内存地址空间,而同一进程中的所有线程在同一块地址空间中工作,这些线程可以共享同一块内存和系统资源。
多线程
概念
定义:
在单个程序中同时运行多个线程完成不同的工作,称为多线程。
实现方法:
继承Thread类和实现Runnable接口
但是由于:
Java语言中只能继承一个类,但可以实现多个接口
一般场景下,我们应尽量选择实现Runnable接口
而且Runnable接口可以实现资源共享。
几个小要点:
每个Java程序最少有一个执行线程。当运行程序的时候, JVM运行负责调用main()方法的执行线程。
无论在任何时候,都可以用Thread.currentThread().getName()来获取当前线程的名字。
实现
继承Thread类
具体步骤:
- 定义一个继承Thread类的子类,并重写该类的run()方法;
- 创建Thread子类的实例,即创建了线程对象;
- 调用该线程对象的start()方法启动线程。
start()方法:用于启动线程,
线程进入就绪状态,待该线程获得CPU,立即启动线程,线程一旦启动,执行该对象的run()方法。
注意:
- 启动线程,调用start方法,不要直接调用run方法
- 不要覆盖start方案(因为start调用底层代码)
- 不要重复调用start方法
测试代码:
class Mythread extends Thread {
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
Scanner sc = new Scanner(System.in);
Mythread my = new Mythread();// 新建状态
my.start();// 就绪状态(cpu不一定会运行这个进程)
}
}
结果
main 1
....
main 10
Thread-0 1
.....
Thread-0 10
可以得出系统正在多线程执行:
因为主函数在输出的Thread-0 x的时候还在进行,(主函数一旦停止,整个程序就停止了。)
利用Runnable接口
基本过程:
- 定义Runnable接口的实现类,并重写该接口的run()方法;
- 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。
测试代码:
class MyRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
// 创建一个MyRunnable对象
MyRunnable my = new MyRunnable();
// 封装出一个线程对象
Thread thread = new Thread(my);
// 启动
thread.start();
}
}
实现共享数据
方法:
将需要共享的变量设为类变量即可。
测试代码:
class MyRunnable1 implements Runnable {
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + "编号为" + i + "的票已经卖了。");
}
}
}
//实现资源共享
class MyRunnable2 implements Runnable {
private int num = 10;// 实现资源共享设为私有的,安全
public void run() {
while (true) {
if (num <= 0)//停止条件
break;
System.out.println("在" + Thread.currentThread().getName() + " " + " 编号为" + num-- + "的票已经卖了。");
//num--一定设置在此语句里面,不然可能出现同一编号的票,被两个窗口卖。
try {
Thread.sleep(1);// 执行一次就休眠1毫秒,尽量让每个窗口都能卖票。
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class Main {
public static void Test1() throws InterruptedException {
System.out.println("三个窗口同时各卖10张票");
MyRunnable1 myRunnable1 = new MyRunnable1();
Thread myTread1 = new Thread(myRunnable1, "窗口1");// 创建线程时同时重命名线程名称为:窗口1
MyRunnable1 myRunnable2 = new MyRunnable1();
Thread myTread2 = new Thread(myRunnable2, "窗口2");// 创建线程时同时重命名线程名称为:窗口2
MyRunnable1 myRunnable3 = new MyRunnable1();
Thread myTread3 = new Thread(myRunnable3, "窗口3");// 创建线程时同时重命名线程名称为:窗口3
myTread1.start();// 启动线程
myTread1.join();// 让该方法的所有线程插队,保证该方法执行完毕之后,再执行方法2
myTread2.start();// 启动线程
myTread2.join();// 让该方法的所有线程插队,保证该方法执行完毕之后,再执行方法2
myTread3.start();// 启动线程
myTread3.join();// 让该方法的所有线程插队,保证该方法执行完毕之后,再执行方法2
}
public static void Test2() throws InterruptedException {
System.out.println("三个窗口共同卖10张票");
MyRunnable2 myRunnable = new MyRunnable2();
Thread myTread1 = new Thread(myRunnable, "窗口1");// 创建线程时同时重命名线程名称为:窗口1
Thread myTread2 = new Thread(myRunnable, "窗口2");// 创建线程时同时重命名线程名称为:窗口2
Thread myTread3 = new Thread(myRunnable, "窗口3");// 创建线程时同时重命名线程名称为:窗口3
myTread1.start();// 启动线程
myTread2.start();// 启动线程
myTread3.start();// 启动线程
}
public static void main(String[] args) throws InterruptedException {
Test1();
System.out.println("=========================");
Test2();
}
}
守护/后台线程
在Java中有两类线程:前台/用户线程 (User Thread)、后台/守护线程 (Daemon Thread)。
概念
后台线程是指: 在程序运行的时候在后台提供一种通用服务的线程。
一般来说这种线程不是程序必须的部分。(比如垃圾回收线程)
前台线程和后台线程关系:
前台线程执行,后台线程执行;
前台线程不执行,后台线程立即停止
注意:
当所有的前台线程结束时,程序也就终止了。(系统会自动:会杀死进程中的所有后台线程。)
实现方法
在start()之前,使用thread.setDaemon(true)
进行设置
线程的调度
线程的生命周期
线程共包括以下5种状态。
-
新建状态(New): 线程对象被创建后,就进入了新建状态。此时它和其他Java对象一样,由Java虚拟机分配了内存,并初始化其成员变量值。
-
就绪状态(Runnable): 也被称为“可执行状态”。处于就绪状态的线程,随时可能被CPU调度执行,取决于JVM中线程调度器的调度。
线程对象被调用了该对象的start()方法,该线程处于就绪状态。
-
运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
-
阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(1) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
(2) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(3) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态(Dead): 该线程结束生命周期。
线程执行完了、因异常退出了run()方法或者直接调用该线程的stop()方法会处于此状态
Thread类调度线程
线程的优先级
虚拟机按照特定机制为线程分配CPU使用权
分时调度(CPU时间片)、抢占式调度(线程优先级)
优先级分为1~10,默认为5;但是需要注意优先级别高的不一定先执行,只是先执行的可能性大。
setPriority(int newPriority)
//设置线程优先级
getPriority()
//返回指定线程的优先级
启动线程
void start()//进入就绪状态
线程让步
void yield();//提示线程调度器当前线程愿意放弃当前CPU的使用,自愿转换为就绪状态
如果当前资源不紧张,调度器可以忽略这个提示。
不阻塞该线程,
线程休眠
void sleep(long mills)//进入休眠等待状态
线程插队
void join() // join线程A,会使得线程B进入等待,直到线程A结束
void join(long mills) //join线程A,会使得线程B进入等待,直到到达给定的时间
可以用来实现:让线程/进程A运行结束再执行线程/进程B。
测试代码:
public class My2 {
public static void main(String[] args) {
MyThreadd myThread = new MyThreadd();
myThread.start();
Thread.yield();//Main线程让步了
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--" + i);
}
}
}
class MyThreadd extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "==" + i);
}
}
}
/*
如果Main中 Thread.yield();存在
*结果1:Thread-0==0
****
Thread-0==9
main--0
****
main--9
*/
/*
如果Main中 Thread.yield();存在
*结果2:
main--0
*****
main--9
Thread-0==0
*****
Thread-0==9
*/
测试join
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--" + i);
if(i==2)
{
myThread.join(1);
}
}
/*结果:main--0
main--1
main--2
Thread-0==0
******
Thread-0==9
main--3
******
main--9
*/
多线程同步
概述
为什么需要多线程同步?
多线程并发执行能够提高执行效率,但会引发安全问题。
比如: 因为线程之间会共享资源,对于“买卖票”的场景,同一时间“买”和“卖”两个线程同时工作
可能会得到:已经没有货物了,但是还能“卖”东西。显然是不对的。
常见线程安全问题:
多窗口卖票问题
同步代码块
同步方法
同步锁
多线程同步作用:
限制某个资源在同一时刻只能被一个线程访问。
实现方法
实现想法:
- 锁住应用,使应用最多只能允许一个线程运行,保证线程运行唯一性。
- 通过让线程等待和运行的命令,让程序有序进行。
实现锁住应用: 使用:synchronized
关键字修饰。
synchronized
作用:
多线程环境下, 线程1 在执行 synchronized 代码块时,线程2 再执行到这段代码块时就会被阻塞。只有当线程1 执行完此同步方法后,才会释放锁对象,线程2 才有可能获取此同步锁。
原理:
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
注意:
synchronized是和if、else、for、while一样的关键字
- 无法中断一个正在等候获得锁的线程
- 无法通过轮询得到锁
4.同时最多只有一个线程能够获得同步锁
实现运行和等待命令: 使用wait和notify方法
wait和notify
wait()
//使当前线程等待使其进入到等待阻塞状态,同时wait()也会让当前线程释放它所持有的锁。 直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。
wait(long timeout)
//让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法唤醒此线程,或者 超过指定的时间量”。
notify()
//唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
notifyAll()
//唤醒所有线程。
所以实现方法: 就是
- 使用:
synchronized
关键字修饰方法,获取该对象的同步锁。 - 使用wait和notify方法有序调配线程进行。
同步途径
我们只需掌握第一个“同步方法”即可。
同步方法(常用)
解释:
用synchronized关键字修饰方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
注意:
如果想让同步线程多次执行,需要再设置继承Thread类的线程控制
class Value {
int value = 0;// 生产对象
boolean flag = false;// 判断生产消费状态,生产完毕(待消费)为TRUE,消费完毕(待生产)为FALSE
// 消费者线程
public synchronized void GetValue() throws InterruptedException {
if (flag == false) {
wait();// 等待
}
System.out.println("消费者已经购买第" + (value) + "件产品");
flag = false;
notify();// 唤醒生产者者线程
}
// 生产者线程
public synchronized void SetValue(int value) throws InterruptedException {
if (flag == true) {
wait();// 等待
}
this.value = value;
System.out.println("生产者已经生产第" + (value) + "件产品");
flag = true;
notify();// 唤醒消费者线程
}
}
//建立生产者线程
class SetThread extends Thread {
Value value;
public SetThread(Value value) {//带参数构造方法
super();
this.value = value;
}
public void run() {
for (int i = 1; i <= 20; i++) {//执行20次
try {
value.SetValue(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//建立消费者线程
class GetThread extends Thread {
Value value;
public GetThread(Value value) {//带参数构造方法
super();
this.value = value;
}
public void run() {
for (int i = 1; i <= 20; i++) {//执行20次
try {
value.GetValue();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Value value = new Value();
SetThread setThread = new SetThread(value);
GetThread getThread = new GetThread(value);
setThread.start();//启动生产者线程
getThread.start();//启动消费者线程
}
}
同步代码块
用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
原因:
同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
public class TicketRunnable implements Runnable {
int count = 10000;
Object lock = new Object();
@Override
public void run() {
while (count > 0) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (10001 - count) + "张票");
count--;
}
}
}
}
同步锁
使用ReentrantLock类
ReenreantLock类的常用方法有:
ReentrantLock()
// 创建一个ReentrantLock实例
lock()
//获得锁
unlock()
//释放锁
优势:
让某个线程在持续获取同步锁失败后返回,不再继续等待
public class TicketRunnable implements Runnable {
int count = 100;
private final Lock lock=new ReentrantLock();
@Override
public void run() {
while (count > 0) {
lock.lock();
System.out.println(Thread.currentThread().getName() + "卖出第" + (101 - count) + "张票");
count--;
lock.unlock();
}
}
}