Java笔记(19)多线程
1.多线程
(1)什么是多线程
在计算机中,当我们执行一个程序时就开启了一个进程,这个进程拥有计算机的一部分系统资源,所以说进程代表着一个正在运行的应用程序。
而线程是进程的执行单元,也就是应用程序的执行路径。
- 单线程:一个应用程序只有一个线程的时候,称为单线程
- 多线程:一个应用程序有多个线程即执行路径的时候,称为多线程;
多线程是一种机制,它允许程序并发的执行多个线程,并且每个线程都是独立的,这样可以有效的提高CPU的利用率。
(2)Java的程序启动是多线程的吗?
首先,在Java中,我们写的代码经过编译后用JVM执行时相当于在计算机中打开了一个应用程序,即每次JVM启动会创建一个进程,而该进程会启动一个主线程。且JVM启动是多线程的,因为除了主线程外,它至少还会启动一个垃圾回收线程。
(3)Java多线程实现方案一
在Java中,主要有两种实现多线程的方式,一种是继承Thread类,一种是继承Runnable接口。
Thread实现多线程:
Thread类是在lang包下的一个类,它实现了Runnable接口,要实现多线程,只需让进行多线程的代码封装在一个类里并继承Thread类,然后重写该类的Run方法即可;
Thread类下方法:
构造方法摘要:
Thread()
分配新的 Thread 对象。
Thread(Runnable target)
分配新的 Thread 对象。
Thread(Runnable target, String name)
分配新的 Thread 对象。
Thread(String name)
分配新的 Thread 对象
成员方法:
static int activeCount()
返回当前线程的线程组中活动线程的数目。
void checkAccess()
判定当前运行的线程是否有权修改该线程。
static Thread currentThread()
返回对当前正在执行的线程对象的引用。
long getId()
返回该线程的标识符。
String getName()
返回该线程的名称。
int getPriority()
返回线程的优先级。
ThreadGroup getThreadGroup()
返回该线程所属的线程组。
static boolean holdsLock(Object obj)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
void interrupt()
中断线程。
static boolean interrupted()
测试当前线程是否已经中断。
boolean isAlive()
测试线程是否处于活动状态。
boolean isDaemon()
测试该线程是否为守护线程。
boolean isInterrupted()
测试线程是否已经中断。
void join()
等待该线程终止。
void join(long millis)
等待该线程终止的时间最长为 millis 毫秒。
void join(long millis, int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
void setName(String name)
改变线程名称,使之与参数 name 相同。
void setPriority(int newPriority)
更改线程的优先级。
static void sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
static void sleep(long millis, int nanos)
在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
String toString()
返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
static void yield()
暂停当前正在执行的线程对象,并执行其他线程。
Thread创建多线程实例
public class MyNewThread extends Thread {
private int x = 10;
public MyNewThread() {
super();
}
public MyNewThread(String name) {
//该构造方法调用Thread的构造,可以设置线程的名称
super(name);
}
//需要执行的代码都写在run里
public void run() {
for(int i = 0; i < x; i++){
//输出当前正在执行的线程对象的名称+数字
System.out.println(currentThread().getName() + ":" + i);
}
}
}
//测试类
public class Demo {
public static void main(String[] args) {
MyNewThread thread1 = new MyNewThread("线程1");
//不能直接调用run方法,直接调用相当于普通调用方法,实现不了多线程
thread1.start();
MyNewThread thread2 = new MyNewThread("线程2");
thread2.start();
//start方法的执行相当于先启动线程,随后由JVM调用该线程的run方法
}
}
//执行结果:
线程1:0
线程1:1
线程1:2
线程2:0
线程1:3
线程2:1
线程1:4
线程2:2
线程1:5
线程2:3
线程1:6
线程2:4
线程1:7
线程2:5
线程1:8
线程2:6
线程1:9
线程2:7
线程2:8
线程2:9
Java的线程调度及线程优先级
在Java中,对于线程的调度采用的是抢占式模型,即优先让线程优先级高的线程优先使用CPU执行,而如果优先级相同,就随机选择一个,线程优先级高的线程对于CPU时间片的使用时长就会长一些;
而Java在多线程实现类上也实现了可以设置线程优先级的方法,接下来演示一个实例:
public static void main(String[] args) {
MyNewThread thread1 = new MyNewThread("线程1");
MyNewThread thread2 = new MyNewThread("线程2");
System.out.println(thread1.getName() + "默认优先级:" + thread1.getPriority());
System.out.println(thread2.getName() + "默认优先级:" + thread2.getPriority());
//设置线程1为线程最大优先级,该字段在Thread类中,值是10
thread1.setPriority(Thread.MAX_PRIORITY);
//设置线程2位线程最小优先级,该字段在Thread类中,值是1
thread2.setPriority(Thread.MIN_PRIORITY);
System.out.println(thread1.getName() + "当前优先级:" + thread1.getPriority());
System.out.println(thread2.getName() + "当前优先级:" + thread2.getPriority());
//启动线程1和线程2,看执行结果
thread1.start();
thread2.start();
}
//结果
线程1默认优先级:5
线程2默认优先级:5
线程1当前优先级:10
线程2当前优先级:1
线程1:0
线程1:1
线程1:2
线程1:3
线程1:4
线程1:5
线程1:6
线程1:7
线程1:8
线程1:9
线程2:0
线程2:1
线程2:2
线程2:3
线程2:4
线程2:5
线程2:6
线程2:7
线程2:8
线程2:9
注意的是,优先级高只是使该线程获得CPU时间片的几率相对高一些,并不一定每一次都是这个结果。
Java线程的线程控制状态
Java多线程中,有五种线程控制状态,分别为:休眠、加入、礼让、守护、中断。
每种状态都在Thread类下的方法可以控制:
休眠:static void sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
加入:void join()
等待该线程终止。即先让该线程执行完再执行其他线程
礼让:static void yield()
暂停当前正在执行的线程对象,并执行其他线程。可以让多个线程间执行更和谐,类似你一次我一次的效果
守护:void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
守护线程执行中,当其他所有非守护线程执行完毕,守护线程也会中止,因为此时java虚拟机会自动退出。
中断:void interrupt()
中断线程,并抛出一个InterruptedException异常。
线程的生命周期:
线程的生命周期主要有以下几种状态:
- 新建
- 就绪
- 运行
- 阻塞
- 死亡
(4)Java多线程实现方案二
实现Runnable接口创建多线程
创建多线程的第二种方式就是自定义类实现Runnable接口,并重写其run方法。
代码示例:
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int x = 0; x < 10; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"线程1");
Thread t2 = new Thread(mr,"线程2");
t1.start();
t2.start();
}
}
//结果:
线程1:0
线程1:1
线程1:2
线程2:0
线程1:3
线程2:1
线程1:4
线程2:2
线程1:5
线程2:3
线程1:6
线程2:4
线程1:7
线程2:5
线程1:8
线程2:6
线程2:7
线程1:9
线程2:8
线程2:9
方式二创建多线程的好处是可以多个线程处理同一个Runnable实现类的资源,可以使数据与处理的线程之间分离,体现了面向对象设计的思想;
(5)Java多线程之同步
在多线程中,当多个线程访问修改同一资源时,容易出现一些错误结果。例如用多线程模拟卖票程序时,就可能会出现漏票多票甚至负票的结果。这是因为CPU每次操作都是原子性的,即当一个线程抢占到CPU时间片后可能正执行到卖票并从数据资源中减掉票时就被其他线程抢占到了CPU,这时就可能出现刚才的线程还没减完票后面的线程就抢占了并再次输出或修改,这就可能导致结果发生一些意外错误结果;为了防止这些错误结果的发生并保证程序的安全性,Java提供了线程同步机制;
在Java中,可以通过synchronized关键字来对需要被同步的代码块进行封装,它保证了被同步的代码块在其完整执行时段内只能被一个线程访问,来避免多个线程的并发修改异常;
同步代码块格式:
synchronized(锁对象){
封装的代码;
}
注意的是,这个锁对象需要是一个固定的,多个线程共有的唯一一个对象,同步锁机制实现的原理靠的就是这个锁对象,它类似当一个线程进入同步锁的代码内,其他线程就不能再进入,锁对象就相当于钥匙,如果有多个锁对象的话那么锁的意义就没有了,锁保证的就是使同一代码只能被一个线程执行,只有执行完毕其他线程才能抢占再执行该锁内的代码;
同步方法:
public synchronized void 方法名(){
代码;
}
同步方法就是将同步关键字加到方法声明上,作用与同步代码块相同,它的锁对象是this,即当前对象;
如果把锁关键字加载静态方法上,则是静态同步方法,它的锁对象是当前类的字节码文件对象,字节码文件对象再反射中会详细介绍。
同步的优点与缺点
同步可以有效解决多线程中的安全问题,但如果线程数量很多的时候,它对效率的影响也很大,因为每执行完一次同步代码几个线程都要去验证锁对象,这无疑会对资源造成一些无形浪费;
同步卖票程序案例:
public class SellTicket implements Runnable {
// 定义票的数量
private int ticket;
// 创建锁对象,也可以用this,因为是用的实现Runnable方式的
private Object obj;
public SellTicket() {
super();
init();
}
public SellTicket(int ticket) {
super();
init();
this.ticket = ticket;
}
// 初始化方法
private void init() {
ticket = 100;
obj = new Object();
}
@Override
public void run() {
while (ticket > 0) {
// 下面的代码被锁起来
synchronized (obj) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}
}
}
}
}
public class Test {
public static void main(String[] args) {
//创建卖票对象
SellTicket st = new SellTicket(1000);
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
(6)Java多线程扩展
1.JDK5以后的针对线程的Lock锁
在JDK5版本后,为了更清晰的表示多线程中是如何加锁和释放锁,就提供了一个新的锁对象Lock。
Lock锁对象版卖票案例
public class SellLockTicket implements Runnable {
private int ticket = 100;
//定义锁对象
private final Lock lock = new ReentrantLock();
public SellLockTicket() {
super();
}
@Override
public void run() {
while(true) {
//为了防止锁内的代码出问题无法释放锁,加一个try...finally语句
try {
//上锁
lock.lock();
//判断票数是否小于0
if(ticket <= 0) {
break;
}
//加一个延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "张票");
}finally {
//释放锁
lock.unlock();
}
}
}
}
//测试
public class Test {
public static void main(String[] args) {
//创建卖票对象
SellLockTicket st = new SellLockTicket();
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
死锁问题:
在线程同步过程中,多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。
从网上其他文档看到的产生死锁的几个条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
例如:
public class MyLockObject {
//定义两个对象作为锁
public static final Object objA = new Object();
public static final Object objB = new Object();
}
//写一个继承Thread的线程类
public class DieLock extends Thread {
private boolean flag;
//控制线程执行哪些代码
public DieLock(boolean flag) {
this.flag = flag;
}
public void run() {
//两个线程分别走两个代码
if(flag) {
//线程1先走A锁
synchronized(MyLockObject.objA) {
System.out.println("第一步的A");
//走完上面的再走B锁的代码
synchronized(MyLockObject.objB) {
System.out.println("第一步的B");
}
}
}else {
//线程2先走B锁
synchronized(MyLockObject.objB) {
System.out.println("第一步的B");
//走完上面的再走B锁的代码
synchronized(MyLockObject.objA) {
System.out.println("第一步的A");
}
}
}
}
}
//这里可能就会出现线程1走到一半的时候线程2抢到了执行权,
然后线程2走完一半时这时线程1又抢到了执行权,这时线程2的objB锁还没释放
而线程1却又需要B锁来走完剩下的代码才能释放A锁,这时就会出现互相等待的情况,使程序
发生了死锁现象
避免死锁的意见:线程每次只锁定一个对象并且锁定过程中不再去锁定其他对象,或者增加先后顺序,即先锁定A然后再锁定B的方式。尽量在开发中避免锁嵌套的使用。
(7)Java多线程的等待唤醒机制
在Java中,我们经常会遇到类似生产者与消费者的问题,例如在之前的卖票程序中,正常的卖票应该是有进有出,即既有进票也有出票,还可能有退票的情况,这时就需要用到等待唤醒机制实现。为了防止在这种情况下出错,每次卖票前要先等待进票线程是否将数据存入,之后通知卖票程序卖出票,这就是等待唤醒机制;
等待唤醒机制主要依赖三个方法:wait()、nofity()、nofityAll();
wait()方法是使当前线程等待,直到该线程被其他线程唤醒才继续执行;
nofity()方法是唤醒当前对象监视器的单个线程;
nofityAll()方法是唤醒当前对象监视器的多个线程;
注意的是,这三个方法是定义在Object类中的,原因是这些方法都与锁对象密切相关,而锁又是任意对象,所以定义在Object类中;
wait()与sleep()的区别是:
wait():不必指定时间,方法执行后会释放锁;
sleep():必须指定时间,不会释放锁。
(8)Java多线程之线程组
Java中,第三种创建线程的方式是线程组,它的类是ThreadGroup,每个线程都有其默认线程组,可以通过Thread类getThreadGroup()方法获取线程的线程组对象。对于未分组的线程其默认是主线程组,即main线程组;如果要想为线程分组,可以通过Thread构造方法设置,具体查看API;
线程组的优势是对于其下的多个线程非常方便管理,可以通过一些方法为其的所有线程设置状态或优先级等;
(9)Java多线程之线程池
在Java中,每次创建一个新线程是很耗费系统资源的,假如我们需要不停的创建新线程,每个线程使用完毕被垃圾回收期回收,然后又新创建线程,对于这样的操作,无疑是很浪费资源的。为了解决这样的问题,Java就提供了线程池,它可以在创建的时候自动或者指定创建一些数量的线程养在池中,使用时从其中直接拿出,使用完毕又回收回线程池,可以避免在创建大量短期生命周期的线程中的资源浪费;
JDK5新增了一个工厂类Executor来产生线程池,其创建线程池的具体步骤如下:
//创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//其下有个方法submit,可以执行Runnable接口或Callable对象的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
//结束线程池
pool.shutdown();
(10)Java多线程实现方案三(Callable)
Callable接口是一个带泛型的接口,它有一个可以计算返回值的方法call;它的使用与Runnable类似,当需要创建一个带返回值的线程时可以使用该方式创建线程;
实现案例:
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for(int x = 0; x < 10; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
return "这是Callable";
}
}
//测试
public class MyExecutorThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<?> f1 = pool.submit(new MyCallable());
Future<?> f2 = pool.submit(new MyCallable());
//得到call的返回结果
System.out.println(f1.get());
System.out.println(f2.get());
pool.shutdown();
}
}
(11)Java多线程之匿名内部类
代码示例:
//1
new Thread(new Runnable() {
@Override
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}).start();
//2
new Thread(){
@Override
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}.start();
//面试题:
new Thread(new Runnable() {
@Override
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}){
public void run() {
for(int y = 0; y < 10; y++) {
System.out.println(y);
}
}
}.start();
//这个最终会执行Thread的大括号里面的代码
(12)Java多线程之定时器的使用
定时器Timer类是一种工具,通过这个工具可以为线程设置后台定时任务,使线程可以在后台运行一段时间后自动执行某个任务。
Timer类方法:
构造方法摘要:
Timer()
创建一个新计时器。
Timer(boolean isDaemon)
创建一个新计时器,可以指定其相关的线程作为守护程序运行。
Timer(String name)
创建一个新计时器,其相关的线程具有指定的名称。
Timer(String name, boolean isDaemon)
创建一个新计时器,其相关的线程具有指定的名称,并且可以指定作为守护程序运行。
成员方法:
void cancel()
终止此计时器,丢弃所有当前已安排的任务。
int purge()
从此计时器的任务队列中移除所有已取消的任务。
void schedule(TimerTask task, Date time)
安排在指定的时间执行指定的任务。
void schedule(TimerTask task, Date firstTime, long period)
安排指定的任务在指定的时间开始进行重复的固定延迟执行。
void schedule(TimerTask task, long delay)
安排在指定延迟后执行指定的任务。
void schedule(TimerTask task, long delay, long period)
安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。
void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
安排指定的任务在指定的时间开始进行重复的固定速率执行。
void scheduleAtFixedRate(TimerTask task, long delay, long period)
安排指定的任务在指定的延迟后开始进行重复的固定速率执行。
代码实现:
//任务类,规定执行什么任务
public class MyTask extends TimerTask {
private Timer t;
public MyTask() {}
//接收一个Timer对象,在任务执行完后使计时器终止
public MyTask(Timer t) {
this.t = t;
}
@Override
public void run() {
for(int x = 0; x < 10; x++) {
System.out.println(x);
}
System.out.println("任务执行完毕");
//终止计时器
t.cancel();
}
}
测试定时器
public class MyTimer {
public static void main(String[] args) {
//创建定时器
Timer t = new Timer();
//调用定时器方法,3秒后执行任务
t.schedule(new MyTask(t), 3000);
}
}
(13)Java多线程之volatile关键字
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。
volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。
volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。
也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。
其中volatile关键字主要用于修饰共享数据变量,synchronized关键字用于修饰方法和代码块。
Java的原子变量
为了解决线程安全中的原子性问题,jdk1.5之后在java.util.concurrent.atomic包下提供了常用的原子变量供我们使用,原子变量可以避免我们在多线程操作中的原子性安全问题。它的底层是支持了CAS算法保证数据的原子性。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。