十、多线程
1、基本概念
一个exe在内存中有一个单独的地址,程序一般对应一个进程
一个进程中如需同时运行多个子程序时,需要开启多个线程
当一个Java程序启动时,JVM会自动创建主线程,并在该线程中调用程序的main()方法
JVM还创建了其他线程,例如,与垃圾收集、对象终止和其它JVM内务处理任务相关的线程。
合理的线程使用可以帮助我们显著的提供程序运行效率
2、线程的实现
Java语言中实现线程的方式有两种,但都是通过start()方法启动线程实例
- 第一种,继承线程类:Thread
继承Thread类:
class Test extends Thread{
public void run(){
while(true){
System.out.println("o");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
实例化:
Test td = new Test();
启动线程
td.start();
- 第二种:实现Runnable接口
实现runable接口:
class Test implements Runnable{
public void run(){
while(true){
System.out.println("o");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
实例化并建立线程对象:
Thread td = new Thread(new Test());
运行线程:
td.start();
Daemon线程
先来看个多线程的例子
public class ParentTest
{
public static void main(String[] args)
{
System.out.println("parent thread begin ");
ChildThread t1 = new ChildThread("thread1");
ChildThread t2 = new ChildThread("thread2");
//t1.setDaemon(true);
//t2.setDaemon(true);
t1.start();
t2.start();
System.out.println("parent thread over ");
}
}
class ChildThread extends Thread
{
private String name = null;
public ChildThread(String name)
{
this.name = name;
}
@Override
public void run()
{
System.out.println(this.name + "--child thead begin");
try
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
System.out.println(e);
}
System.out.println(this.name + "--child thead over");
}
}
执行结果如下:
parent thread begin
parent thread over
thread1--child thead begin
thread2--child thead begin
程序在主程序运行结束后,子程序依然进行;
若在主线程中创建了子线程,如果希望在主线程结束时,子线程也结束,这时候就需要Daemon线程
一个Daemon线程是一个在后台执行服务的子线程。 如果所有的非Daemon的线程都结束了,则Daemon 线程就会自动终止。
例如:main方法是一个非Daemon线程,如果希望main方法结束后子线程跟着终止,要将它设为Daemon线程。
Daemon线程设置方法: Thread.setDaemon(true);
- 第三种:实现Callable接口
比较推荐实现Runnable接口的方式,原因如下:
(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数
据有效的分离,较好地体现了面向对象的设计思想。 (可联想到模拟火车站卖票的例子)
(2)可以避免由于Java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经
继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread
类的方式,那么,这个类就只能采用实现Runnable接口的方式了。 (单继承多实现)
(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代
码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码
无关。当共享访问相同的对象时,即它们共享相同的数据。当线程被构造时,需要的代码和数据
通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。
4、线程的生命周期
-
生命的几个基本状态
-
运行中的方式
1、执行了start()之后,线程进入了Runnable状态,此时线程尚未真正开始执行
2、必须等待排班器(Scheduler)的排班,决定运行的先后顺序
有几种状况会让线程进入Blocked状态:
等待输入输出;
调用sleep()方法;
尝试取得对象锁定;
调用wait()方法;
Blocked线程如何恢复到Runnable状态?
输入输出完成;
调用interrupt();
取得对象锁定;
调用notify()或notifyAll();
5、线程的休眠与唤醒
Thread thread = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(99999);
}catch(InterruptedException e) {
System.out.println("I'm interrupted!!");
}
System.out.println("Hello Thread");
}
});
thread.start();
thread.interrupt();
6、线程的加入
- 当线程使用join()加入至另一个线程时,另一个线程会等待这个被加入的线程工作完毕,然后再继续它的动作。
- join()的意思表示将线程加入成为另一个线程的流程之一
Thread threadB = new Thread(new Runnable() {
public void run() {
…
}
});
threadB.start();
try {
threadB.join(); //子线程threadB加入主线程main中
}catch(InterruptedException e) {
e.printStackTrace();
}
7、线程的停止
- 不建议使用stop()来停止一个线程的运行,而是采用向run方法传递一个Boolean型的信号量,以控制其结束与开始
public class SomeThread implements Runnable {
private boolean flag = true;
public void stopThread() {
this.flag = false;
}
public void run() {
while(isContinue) {
// ... some statements
}
}
}
8、 线程的同步化 (关键字synchronized)
同步化的作用
- 一个进程中的多个线程共享相同的内存地址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。
- 如果没有同步,数据很容易就处于不一致状态。例如,如果一个线程正在更新两个相关值,而另一个线程正在读取这两个值,有可能在第一个线程只写了一个值,还没有写另一个值的时候,调度第二个线程运行,这样它就会看到一个旧值和一个新值。
- 如果一个对象所持有的数据可以被多线程同时共享存取时,必须考虑到「数据同步」的问题。
对象锁(lock)
实现方式
- 使用“synchronized”关键词为方法加同步锁
public class ThreadTest implements Runnable {
public synchronized void run() {
for (int i = 0; i < 10; i++) {
System.out.print(" " + i);
}
}
public static void main(String[] args) {
Runnable r = new ThreadTest();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
- 使用“synchronized”关键词为语句块加同步锁
public class ThreadTest implements Runnable {
public void run() {
synchronized(this){
for (int i = 0; i < 10; i++) {
System.out.print(" " + i);
}
}
}
public static void main(String[] args) {
Runnable r = new ThreadTest();
new Thread(r).start();
new Thread(r).start();
}
}
9、线程的等待与恢复
- wait()、notify()与notifyAll()是由Object类别所提供的声明为“final”的方法。
- 在同步化的方法或区块中呼叫wait()方法,当前的线程会被放入对象的等待池中,使线程暂停。
- 当notify()方法被调用,JVM会通知等待池中的线程加入,回到锁定池的Blocked状态。
- 被通知的线程是随机的,被通知的线程会与其它线程共同竞争对象的锁定。
- 如果您呼叫notifyAll(),则所有在等待池中的线程都会被通知回到锁定池的Blocked状态。
例子:
package com.phy.thread;
import java.util.Scanner;
/**
* @author :xp
* @date :Created in 2018/10/26 11:00
* @description:
*/
class MT implements Runnable{
public boolean flog = false;
public void run() {
int i = 0 ;
while(true){
System.out.println("Hello World"+i);
i++;
if(flog){
synchronized(this){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TestDaemon {
public static void main(String[] args){
MT mt = new MT();
Thread t = new Thread(mt);
t.start();
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("输入0暂停");
int n = sc.nextInt();
if(n==0) mt.flog = true;
System.out.println("输入1继续");
n = sc.nextInt();
if(n==1){
synchronized (mt) {
mt.flog = false;
mt.notify();
}
}
}
}
}
10、线程的优先级
- 线程有其优先权,由1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY)
- 优先权越高,排班器越优先排入执行,如果优先权相同,则轮流执行(Round-robin方式)
- 可以通过线程的setPriority()方法设置某个线程的优先级,这样,当一个资源空闲时,同时等待这个资源的两个线程里边优先级高的会先获得对象执行权
11、容器类的线程安全
- Collections提供了一系列synchronized方法来传回一个同步化的容器对象,从而保证了容器类的线程安全。
- 使用Iterator遍访对象时,必须实现同步化。
synchronized(list) {
Iterator i = list.iterator(); while (i.hasNext()) {
System.out.println(i.next());
}
}
线程案例:写出输出顺序为男,女,男,女的线程
- 方式一
//首先要定义一个User对象这里省略
package com.phy.thread;
import com.phy.entity.User;
/**
* @author xp
* @date :Created in 2018/10/26 14:52
* @description:设计一个线程类,第一次输出小明男提问
* 第二次输出小红女在提问
*/
//定义一个线程类
class RunnableExample implements Runnable {
private boolean flag = true;
User userB = new User("小明","男",1);
User userG = new User("小红","女",1);
public RunnableExample() {
}
@Override
public synchronized void run() {
while (true){
if(flag) {
System.out.println(userB.getName() + userB.getSex() + "正在问问题");
}else {
System.out.println(userG.getName() + userG.getSex() + "正在问问题");
}
flag = !flag;
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试函数
public class Test2 {
public static void main(String args[]){
Thread threadG = new Thread(new RunnableExample());
threadG.start();
}
}
- 方式二
//首先要定义一个User对象这里省略
package com.phy.thread;
import com.phy.entity.User;
/**
* @author :xp
* @date :Created in 2019/3/26 14:52
* @description:设计一个线程类,输出顺序为男,女,男,女
* 第二次输出小红女在提问
*/
//定义一个线程类
class RunnableExample implements Runnable {
private User user;
boolean flag ;
User userB = new User("小明","男",1);
User userG = new User("小红","女",1);
public RunnableExample(User user,boolean flag) {
this.user = user;
this.flag =flag;
}
@Override
public void run() {
synchronized(user){
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if( flag ){
System.out.println(userB.getName() + userB.getSex() + "正在问问题");
}else {
System.out.println(userG.getName() + userG.getSex() + "正在问问题");
}
user.notifyAll();
try {
user.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//测试函数
public class Test2 {
public static void main(String args[]){
User c= new User();
Thread threadB = new Thread(new RunnableExample(c,true));
Thread threadG = new Thread(new RunnableExample(c,false));
threadB.start();
threadG.start();
}
}
写两个线程,一个线程打印 1~52,另一个线程打印字母A-Z。打印顺序为12A34B56C……5152Z
- 这是线程间的通信的一个很有代表性的例子。过程如下:
1、创建两个线程实现Runnable接口重写run方法,一个用于打印数字,一个用于打印字母。
2、创建一个测试类,在测试类中创建一个Object类的对象(作为两个线程的共享资源,以便实现线程间的通信),通过各类的构造方法传递过去。
3、在两个类的run方法中都要用synchronized保证同步,即加锁。
4、在数字类中用for循环每打印两个数字就唤醒其他线程,释放锁,进入阻塞状态。 在字母类中每打印一个字母就唤醒其他线程,释放锁,进入阻塞状态。 - 在写这个程序的时候有几点要注意的地方:
1、两个线程要使用同一个资源才需相互通信,所以在测试类中创建共享资源,并通过构造方法分别传到各线程类中。
2、两个线程哪个先运行(执行start())哪个就先获得资源并执行
3、在run方法体内写进程间的通信wait()和notifyall()时,一定要先写notifyall()再写wait()。
原因:当你先写wait()时,本进程也进入休眠状态,再写notifyall()唤醒所有线程时本线程以及其他线程被一块唤醒,竞争同一个资源,就会造成死锁。 所以一定要先唤醒其他线程,再让本线程阻塞!
/**
* @author :xp
* @date :Created in 2018/10/26 16:12
* @description:实现两个线程之间的通信
*/
public class ThreadExample {
public static void main(String[] args) {
Object obj = new Object();
Number s = new Number(obj);
Char z = new Char(obj);
Thread th1 = new Thread(s);
Thread th2 = new Thread(z);
th1.start();//数字的线程先运行,数字先执行
th2.start();
}
}
//数字类实现runnable接口
class Number implements Runnable{
private Object obj;
public Number() {
}
public Number(Object obj) {
this.obj = obj;
}
@Override
public void run() {
synchronized(obj){//给共享资源上锁
for(int i = 1;i < 53;i++ ){
System.out.println(i);
if(i % 2 == 0){//保证输出两个数字
obj.notifyAll();//唤醒其他线程
try {
obj.wait();//等待并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//字母类
class Char implements Runnable{
private Object obj;
public Char() {
}
public Char(Object obj) {
this.obj = obj;
}
@Override
public void run() {
synchronized(obj){
for(int i = 0;i < 26;i++ ){
System.out.println((char)(i+'A'));
obj.notifyAll();//唤醒其他线程
try {
obj.wait();//释放锁等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}