2020/4/27-- JAVA多线程
知识要点:
- 线程的创建和启动
- 线程的状态和转换
- 线程的调度和优先级
- 线程的同步
- 集合类的同步问题
- java.util.Timer类调度任务
线程和进程的区别
操作系统中运行多个任务(程序)叫多进程。在一个应用程序中多条执行路径并发执行叫多线程。
区别:
- 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换开销大。
- 同一个进程内的多个线程共享相同的代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程间的切换开销小。
何时使用多线程(通常):
- 程序需要执行多个任务
- 程序需要实现一些等待的任务,如用户输入、文件读写、网络操作、搜索等。
- 需要一些后台运行的程序。
线程的启动和创建
java.lang.Thread类来实现。
Thread类有如下特性:
- 每个线程都是通过某个特定Thread对象的run()方法来完成其操作的。经常把run()方法的主体称为线程体。
- 通过Thread对象的start()方法来调用这个线程。
创建新线程
两种创建新线程的方法。
-
定义实现java.lang.Runnable接口的类。Runnable接口中只有一个run()方法,用来定义线程运行体。
class MyRunner implements Runnable { public void run() { for (int i = 0; i < 100; i++) { System.out.println("MyRunner:" + i); } } }
定义好这个类,把它的实例作为参数传入Thread的构造方法中创建一个新线程。
Thread thread1 = new Thread(new MyRunner());
-
将类定义为Thread类的子类并重写run()方法。
class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println("MyThread:" + i); } } }
这种方法下:
Thread thread2 = new MyThread();
更建议使用第一种方法。采用接口的方式可以避免java的单一继承带来的局限。
启动线程
调用Thread 实例的start方法即可。
thread1.start();
JVM采用时间片调度策略,看起来想是在同时执行。
Thread类介绍
Java.lang.Thread类就是线程实现类。
public void start()
:启动该线程。不一定立马启动,看CPU如何调度。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用public ClassLoader getContextClassLoader()
:返回该线程的上下文的CLassLoader。上下文ClassLoader由线程创建者提供,供运行于该线程中的代码再加载类和资源时使用。public final boolean isAlive()
:测试线程是否还活着public Thread.State getState()
:返回该线程的当前状态public final void String getName()
:返回该线程的名称。public final void settName(String name)
:设置该线程名称public final void setPriority(int newPriority)
:更改线程的优先级public final void setDaemon(boolean on)
:将该线程标记为守护线程或用户线程。public static void sleep(long millis) throws InterruptedException
:在指定的毫秒数内让当前正在执行的线程休眠public void interrupt()
:中断线程
多线程的好处
- 提高应用程序响应。
- 提高计算机系统CPU的利用率,充分利用多CPU运算快的特点。
- 改善程序结构。
线程分类
-
用户线程:Java虚拟机在它所有非守护线程都已经离开后自动离开
-
守护进程:守护线程是来服务用户线程的,如果没有其它用户线程在运行,那么没有可服务对象,也没有理由继续下去。
thread.setDeamon(true);
java的垃圾回收线程就是一个典型的守护线程。
线程的生命周期
生命周期的六种状态,JDK中用Thread.State枚举表示出了这6找那个状态。
NEW
:至今尚未启动的线程处于这种状态RUNNABLE
:可运行状态,start()后的状态BLOCKED
:受阻塞并等待某个监视器锁的线程处于这种状态,阻塞态WAITING
:无限期地等待另一个线程来执行某一特定操作的线程,等待状态TIMED_WAITING
:等待另一个线程来执行取决于指定等待时间的操作者的线程,超时等待状态。TERMINATED
:已退出的线程,终止状态。
状态转换
-
新线程
创建了一个Thread类(或子类)的实例时。线程体中的代码并未得到JVM的执行。
-
可运行的线程
start()后的线程,不一定立即获得CPU,需要操作系统为该线程赋予运行的时间。
-
被阻塞和等待状态下的线程
暂时停止,不会执行线程体内的代码,消耗最少的资源。
- 可运行的线程获取某个对象锁时,该对象锁被其它线程占用,JVM会把该线程放入锁池中。这种状态也叫同步阻塞状态。
- 一个线程需要别的线程来通知它的调度时,它就进入等待状态。可通过Object.wait()和Thread.join()方法实现。实际使用中阻塞和等待状态的区别不大。
- 几种带时超值的方法为导致线程进入定时等待状态。这种状态维持到超时过期或收到适当的通知为止。Object.wait(long timeout)、Thread.join(long timeout)、Thread.sleep(long millis)
-
被终止的线程
- run()方法执行完毕
- 异常事件导致run()终止,导致线程突然死亡
判断是否处于可运行状态或阻塞状态,可以调用Thread类提供的isAlive()方法,如果处于可运行状态或阻塞状态,它返回true;如果处于新建状态或被终止状态,它将返回false。
线程睡眠
调用sleep()方法,传入一个毫秒数作为当前线程睡眠的时间。
线程让步
Thread.yield()方法会暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。把使用CPU的机会让给其它线程。
线程的加入
join()方法,可以将交叉执行的线程变成顺序执行。这里并行变为串行。
线程的调度和优先级
JVM对多个线程进行系统级的协调,以避免因多个线程争用有限的资源而导致应用系统死机或者崩溃。
java定义线程分为10个等级(1~10)。数字越大优先级越高。
Thread类中定义了一个表示线程最低、最高和普通优先等级:MIN_PRIORITY、MAX_PRIORITY、NOMAL_PRIORITY分别代表1,10,5.
一个线程被创建默认优先级是5。
setPriority()方法来改变该线程的运行优先级.
getPriority()方法来获取当前线程的优先级。
public class ThreadPriorityTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new R());
Thread thread2 = new Thread(new R());
thread1.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.start();
}
class R implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
不同操作系统的线程优先级等级可能跟JAVA线程的优先级别不匹配,甚至有的操作系统会完全忽略线程的优先级。所以,为了提高程序的移植性,不太建议手工调整线程的优先级。
线程同步
为了访问共享变量时不出问题。
线程同步的方法
关键字:synchronized来加保护伞,保证数据安全。
主要用于同步代码块和同步方法中。
-
同步方法:synchronized放在方法声明中,表面为同步方法。
public synchronized boolean sell() { boolean flag = true; if (tickets < 100) { tickets++; System.out.println(Thread.currentThread().getName()+ ":卖出第"+ tickets + "张票"); } else flag = false; try {Thread.sleep(15);} catch (InterruptedException e) { e.printStackTrace(); } return flag; }
一个线程调用了synchronized修饰的方法,它就能够保证该方法在执行完毕前不会被另一个线程打断,这种运行机制叫做同步线程机制;没有加synchronized修饰的可以被其它线程打断,这种机制叫做异步线程机制。
-
同步代码块
把线程体内执行的方法会操作到共享数据的语句封装在{}之内,然后用synchronized关键字修饰。
public boolean sell() { boolean flag = true; synchronized(this){ if (tickets < 100) { tickets++; System.out.println(Thread.currentThread().getName()+ ":卖出第"+ tickets + "张票"); } else flag = false; } try {Thread.sleep(15);} catch (InterruptedException e) { e.printStackTrace(); } return flag; }
对象锁
同步实现的机制主要是利用到了“对象锁”。
JVM为每个对象都关联到一个锁。线程拥有当前对象的锁时,不允许其他线程进行访问这个对象的实例变量,即在某个时间点上,一个对象的锁只能被一个线程拥有,
对象锁是JVM内部使用的。只需要告诉JVM哪一块是监视区域,在进入监视区域时,java虚拟机都会自动锁上对象或者类。
显示加锁机制:java.util.concurrent.locks.Lock接口提供的lock()方法来获取锁,unlock()方法来释放锁。
private Lock lock = new ReentrantLock(); //创建Lock实例
public boolean sell() {
boolean flag = true;
lock.lock();
if (tickets < 100) {
tickets++;
System.out.println(Thread.currentThread().getName()+ ":卖出第"+ tickets + "张票");
}
else flag = false;
lock.unlock();
try {Thread.sleep(15);} catch (InterruptedException e) {
e.printStackTrace();
}
return flag;
}
wait和notify方法
常与synchronized关键字结合使用。
java.lang.Object类中提供了wait、notify、notifyAll()方法,这些方法只有在synchronized方法或者代码块中才能使用。
调用wait方法时,当前线程阻塞,并且放弃该对象的锁。
当另外的线程执行了摸一个对象notify方法后,会唤醒在此对象等待池中的某个线程使之成为可运行的线程。notifyAll()方法会唤醒所有等待这个对象的线程(等待此对象的对象等待池)使之成为可运行的线程。
生产者消费者问题:生产者将产品交给店员,消费者从店员处取走产品,店员一次只能持有固定数量的产品,生产者生产了过多的产品,店员会叫生产者等一下,如果店中没有产品了,店员会告诉消费者等待一下,如果有产品了再来取走产品。
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk(); //定义一个服务店员
Thread producerThread = new Thread(new Producer(clerk)); //生产者线程
Thread consumerThread = new Thread(new Consumer(clerk)); //消费者线程
producerThread.start();
consumerThread.start();
}
class Clerk { //店员
private int product = 0;
public synchronized void addProduct() {
if (this.product >= 20) {
try {
wait(); //产品满了 稍后在生产
} catch(InterruptedException e) {
e.printStackTrace();
}
} else {
product++;
System.out.println("生产者生产第"+product + "个产品");
notifyAll(); //通知等待区的消费者可以消费了
}
}
public synchronized void getProduct() {
if (this.product <= 20) {
try {
wait(); //没有产品 稍后在取
} catch(InterruptedException e) {
e.printStackTrace();
}
} else {
product--;
System.out.println("消费者消费"+product + "个产品");
notifyAll(); //通知等待区的生产者可以生产了
}
}
class Producer implements Runnable {
private Clerk clerk;
public Producer(Clerk clerk) {this.clerk = clerk;}
public void run() {
System.out.println("生产者开始生产产品");
while(true) {
try {
Thread.sleep((int) (Math.random()*10) *100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct(); //生产产品
}
}
}
class Producer implements Runnable {
private Clerk clerk;
public Producer(Clerk clerk) {this.clerk = clerk;}
public void run() {
System.out.println("消费者开始取走产品");
while(true) {
try {
Thread.sleep((int) (Math.random()*10) *100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct(); //取产品
}
}
}
}
}
集合类的同步问题
集合类常用,有些是线程同步的(线程安全的)有些是线程不同步的(不安全的)。
不安全的:HashSet、ArrayList、LinkedList、HashMap等线程都是线程不同步的。编写多线程的程序时,需要自行实现同步以确保共享集合类对象在多线程下存取不会出错。
三种方法解决:
-
synchronized同步块
private List<String> list = new ArrayList<String>(); public void run() { synchronized(list){ list.add(..); } }
-
集合工具类同步化集合类对、
几个静态方法把一个非线程同步的集合类对象包装成同步的集合类对象。
- synchronizedSet(Set
s); - synchronizedList(List
list); - synchonizedMap(Map<K, V> m);
在使用这种方法返回的对象进行迭代时仍需要synchronized加锁
private List list = Collections.synchronizedList(new ArrayList()); public void run() { synchronized(list){ Iterator it = list.iterator(); while (i.hasNext()){ i.next(); } } }
- synchronizedSet(Set
-
并发集合类
java.util.concurrent这个包提供了一些确保线程安全的并发集合类。
CopyOnWriteArraySet
CopyOnWriteArrayList
ConcurrentHashMap
private List<String> list = new CopyOnWriteArrayList<String>(); public void run() { Iterator it = list.iterator(); while (i.hasNext()){ i.next(); } }
用Timer类调度任务
java.util.Timer类来定时执行任务。
java.util.TimerTask类是一个抽象类,创建定时任务时,只需要继承这个类并实现run方法,再把要定时执行的任务代码添加到run()方法体中即可。
class MyTask extends TimerTask { //定时任务类
public void run() { //实现run()方法
System.out.println("起床了..")
}
}
public void schedule(TimerTask task, long delay, long period);
重复地以固定的延迟时间去执行一个任务。public void scheduleAtFixedRate(TimerTask task, long delay, long period)
:重复地以固定的频率去执行一个任务。public void cancel()
:终止此计时器,丢弃所有当前以及安排的任务。
import java.util.Timer
import java.util.TimerTask;
import java.IO.IOException
//定时任务调度
public static void main(String[] args) {
Timer timer = new Timer();
timer.scheduleAtFixedRate(new MyTask(), 0, 1000); //每1000毫秒就重复执行一次
while (true) {
try {
int ch = System.in.read();
if (ch == 'q') {
timer.cancle(); //取消任务
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}