最近要面试,所以整理一下多线程的知识点。
进程:正在执行中的程序,其实是应用程序在内存中运行的那片空间
线程:进程中的一个执行单元,负责进程中的程序的运行,一个进程中至少要有一个线程
多线程:一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序
多线程的应用:实现多部分程序同时执行,专业术语称之为并发
多线程的使用可以合理使用CPU的资源,如果线程过多会导致性能降低
CPU处理程序时是通过快速切换完成的,在我们看来好像随机一样
线程状态图
一 多线程技术解决的问题
不使用多线程,JVM启动后,必须有一个执行路径(线程)从main方法开始,一直执行到main方法结束,这个线程在java中称为主线程。
当主线程在这个程序中执行时,如果遇到了循环而导致在指定位置停留时间过长时,将无法执行下面的程序,那么,可不可以实现一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行?由此来实现多部分代码同时执行,这就是多线程技术可以解决的问题。
代码示例
/*
* 演示使用多线程与不使用的区别
*/
public class ThreadDemo1 {
public static void main(String[] args) {
/*
* 不使用多线程的调用方式 由于主线程按顺序执行 运行下面代码将先打印20次小强 再打印20次旺财
*/
// Person1 d1=new Person1("小强");
// Person1 d2=new Person1("旺财");
// d1.show();
// d2.show();
// System.out.println("----------------");
/*
* 使用多线程方式 由于采用多线程方式,运行结果每次都不相同
*/
Person2 t1 = new Person2("小强");
Person2 t2 = new Person2("旺财");
t1.start();// 由t2这个线程开启
t2.run();// 由主线程负责
}
}
/*
* 用于演示非多线程方式的类
*/
class Person1 {
private String name;
public Person1(String name) {
this.name = name;
}
public void show() {
for (int i = 0; i < 20; i++) {
System.out.println(name + "..." + i);
}
}
}
/*
* 用于演示多线程方式的类 继承Thread类的方式来实现线程
*/
class Person2 extends Thread {
private String name;
public Person2(String name) {
this.name = name;
}
public void show() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ":" + name
+ "..." + i);
}
}
/*
* 重写run方法
*/
@Override
public void run() {
show();
}
}
二 实现多线程的两种方式
1.继承Thread类
(1)步骤:
定义一个类继承Thread
重写run方法
创建子类对象(就是创建线程对象)
调用start方法(开启线程并让线程执行,同时告诉JVM去调用run方法)
(2)面试问题:
线程调用run方法和调用start方法的区别:
调用run方法不开启线程,仅仅是对象调用方法
调用start开启线程,并让JVM调用run方法在开启的线程中执行
(3)为什么要继承Thread类
因为Thread类描述线程的事物,具备线程应该有的功能
(4)为什么不只创建Thread类对象
Thread t1=new Thread();
t1.start();
这么做没有错,但是该start方法调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是,这个run方法中并没有定义我们需要让线程执行的代码。
(5)创建线程的目的
是为了建立单独的执行路径,让多部分代码实现同时执行,
也就是说,线程创建并执行需要给定的代码(线程的任务),
对于主线程,他的任务定义在main函数中,
自定义的线程,需要执行的任务都定义在run方法中,
Thread类中的run方法内部的任务并不是我们所需要的,所以重写这个run方法,既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可,所以进行了重写run方法的动作。
(6)多线程执行时的内存分配
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间来进行方法的压栈和弹栈,当执行线程的任务结束了,线程自动在栈内存中释放了,当所有的执行线程都结束了,那么进程就结束了。
(7)常用方法
获取当前执行的线程
Thread.currentThread()
获取线程名
Thread.currentThread().getName()
主线程的名称:main
自定义的线程:Thread+数字,数字从0开始,线程多个时,数字顺延
发生异常的时候,结束的是当前发生异常的线程
(8)代码示例
/*
* 使用继承Thread类的方式实现多线程
*/
public class ThreadDemo1 {
public static void main(String[] args) {
Person t1 = new Person("小强");
Person t2 = new Person("旺财");
t1.start();// 由t2这个线程开启
t2.run();// 由主线程负责
}
}
/*
* 用于演示多线程 ——继承Thread类的方式来实现多线程
*/
class Person extends Thread {
private String name;
public Person(String name) {
this.name = name;
}
public void show() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() +
":" + name+ "..." + i);
}
}
/*
* 重写run方法
*/
@Override
public void run() {
show();
}
}
2.实现Runnable接口
(1)步骤
定义类实现Runnable接口(避免了继承Thread类的单继承局限性),
覆盖接口中run方法,将线程任务代码定义到run方法中,
创建Thread类的对象(只有创建了Thread类的对象才可以创建线程),
将Runnable接口的子类对象作为参数传递给Thread类的构造函数,
调用Thread类的start方法开启线程。
(2)实现Runnable接口的好处
避免了单继承的局限性,所以较为常用,
实现Runnable接口的方式更加符合面向对象,
线程分为两部分:线程对象、线程任务,
继承Thread类,线程对象和线程任务耦合在一起,一旦创建Thread类子类对象,既有线程对象,又有线程任务,
实现Runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型,Runnable接口对线程对象和线程任务进行解耦。
(3)原理
原理类似如下代码,将Runnable的子类作为参数传递,当Runnable子类对象不为空时,将调用Runnable子类对象的run()方法。
public class Thread implements Runnable {
Runnable target = null;
public Thread(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
(4)代码示例
/*
* 实现多线程——使用实现Runnable接口的方式
*/
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建Runnable子类的对象
// 注意:它不是线程对象
Person p1 = new Person("小强");
Person p2 = new Person("旺财");
// 创建Thread类的对象
// 将Runnable接口的子类对象作为参数传递给Thread类的构造函数
Thread t1 = new Thread(p1);
Thread t2 = new Thread(p2);
// 启动线程
t1.start();
t2.start();
// 打印一下主线程名字
System.out.println(Thread.currentThread().getName() + "....");
}
}
/*
* 实现了Runnable接口的子类
*/
class Person implements Runnable {
private String name;
public Person(String name) {
this.name = name;
}
public void show() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ":" + name
+ i);
}
}
// 覆盖了接口runnable接口中的run方法
@Override
public void run() {
show();
}
}
三 多线程的安全问题多线程安全问题产生的原因:
(1)多个线程在操作共享的数据
(2)线程任务操作共享数据的代码有多条(运算有多个)
解决思路
只要让一个线程在执行线程任务时将多条操作共享数据的代码执行完,
在执行的过程中,不要让其他线程参与运算。
1.同步代码块
Java中解决同步问题是通过代码块来完成的,这个代码块叫同步代码块:synchronized
格式:
synchronized(对象)
{
需要同步的代码块
}
好处:解决了多线程的安全性问题
弊端:降低了程序的性能
同步的前提
必须保证多个线程在同步中使用的是同一个锁,
当多线程安全问题发生时,加入了同步后,问题依旧,
这时,就要通过这个同步的前提来判断是否写正确 。
代码示例
/*
* 同步代码块示例
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 创建Runnable接口的子类对象
SaleTicket st = new SaleTicket();
// 创建线程对象
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
Thread t4 = new Thread(st);
// 启动线程
/*
* 如果线程被重复启动, 无效的线程状态异常 IllegalThreadStateException
*/
// t1.start();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class SaleTicket implements Runnable {
// 描述票的数量
private int tickets = 100;
private Object obj = new Object();
// 售票的动作,即线程任务代码
// 线程任务中通常都有循环结构
@Override
public void run() {
while (true) {
// 使用同步代码块解决安全性问题
// obj相当于锁
synchronized (obj) {
if (tickets > 0) {
try {
// 让线程在这里稍停,模拟线程安全性问题的发生
//如果不加同步,会出现0 -1 -2等错误的数据
//这就是多线程安全问题
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "..." + tickets--);
} catch (Exception e) {
}
} else {
break;
}
}
}
}
}
2.同步函数
同步的另一种体现形式,使函数具有同步性:同步函数。
同步函数其实锁的是this,因为函数必须被对象调用。
验证:
写一个同步代码块,写一个同步函数,
如果同步代码块中的锁对象与同步函数中的锁对象是同一个,
那么就同步了,就没有错误的数据,
如果不是同一个锁对象,就会出现错误数据。
过程:
让两个线程,一个线程在同步代码块中执行,一个线程在同步函数中执行。
同步函数和同步代码块的区别:
同步函数使用的锁是固定的this,
当线程任务只需要一个同步时,完全可以使用同步函数。
同步代码块使用的锁可以是任意对象,
当线程任务中需要多个同步时,必须用锁来区分,
这时必须使用同步代码块。
静态同步函数
使用的锁不是this,而是字节码文件对象,类名.class。
代码示例
public class ThreadDemo4 {
public static void main(String[] args) throws Exception {
// 创建Runnable接口的子类对象
Ticket st = new Ticket();
// 创建线程对象
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
// 启动线程
t1.start();
// 切换标记前让主线程停一会,防止主线程执行完,线程t1还没开启的情况
Thread.sleep(10);
// 切换标记
st.flag = false;
t2.start();
}
}
/*
* 同步代码块的演示
*/
/*
class Ticket implements Runnable {
// 描述票的数量
private int tickets = 100;
private Object obj = new Object();
boolean flag = true;
// 售票的动作,即线程任务代码
@Override
public void run() {
if (flag) {
while (true) {
// 当锁的obj的时候,出现了错误数据
// 说明同步函数使用的锁不是obj
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "..obj.." + tickets--);
} catch (Exception e) {
}
}
}
}
} else {
while (true) {
this.sale();
}
}
}
*/
/*
* 将线程任务定义成同步函数的模式
*/
/*
public synchronized void sale() {
if (tickets > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "..syn.."
+ tickets--);
} catch (Exception e) {
}
}
}
}
*/
/*
* 静态同步代码块的演示
*/
class Ticket implements Runnable {
// 描述票的数量
private static int tickets = 100;
private Object obj = new Object();
//通过修改标志位flag的值来改变程序的运行:执行了同步函数还是执行了同步代码块
boolean flag = true;
// 售票的动作,即线程任务代码
@Override
public void run() {
//如果flag为true,执行同步代码块
//如果为false,执行同步函数
if (flag) {
while (true) {
/*
* 静态同步函数使用的锁不是this 而是字节码文件 类名.calss
*/
synchronized (Ticket.class) {
if (tickets > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()
+ "..obj.." + tickets--);
} catch (Exception e) {
}
}
}
}
} else {
while (true) {
this.sale();
}
}
}
/*
* 将线程任务定义成同步函数的模式
*/
public static synchronized void sale() {
if (tickets > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "..syn.."
+ tickets--);
} catch (Exception e) {
}
}
}
}
3.同步的弊端——死锁
同步的另一个弊端:死锁
当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步,
这是很容易引发死锁
例如:
Thread-0执行到
synchronized(obj1){
synchronized (obj2) {
}
}
Thread-1执行到
synchronized(obj2){
synchronized (obj1) {
}
}
Thread-0获取了锁obj1,等待锁obj2,
Thread-1获取了锁obj2,等待锁obj1,
互相等待对方持有的锁,造成了死锁。
代码示例
/*
* 死锁代码范例
*/
public class Test4 {
public static void main(String[] args) {
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
/*
* 多线程死锁示例类
*/
class DeadLock implements Runnable {
private boolean flag;
DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
synchronized (Lock.LOCKA) {
System.out.println(Thread.currentThread().getName()
+ ":if..locka");
synchronized (Lock.LOCKB) {
System.out.println(Thread.currentThread().getName()
+ ":if..lockb");
}
}
}
} else {
while (true) {
synchronized (Lock.LOCKB) {
System.out.println(Thread.currentThread().getName()
+ ":else..lockb");
synchronized (Lock.LOCKA) {
System.out.println(Thread.currentThread().getName()
+ ":else..locka");
}
}
}
}
}
}
/*
* 定义锁对象
*/
class Lock {
public static final Object LOCKA = new Object();
public static final Object LOCKB = new Object();
}
四 等待唤醒机制
1.等待唤醒机制
(1)使用方法
wait():让线程处于等待状态,其实就是将线程临时存储到了线程池中
notify():会唤醒线程池中任意一个等待的线程
notifyAll():会唤醒线程池中所有的等待线程
总结:这些方法必须使用在同步中,因为必须要标识wait(),notify()等方法所属的锁;
同一个锁上的notify只能唤醒该锁上的被wait的线程。
(2)为什么这些方法被定义在Object类中?
因为这些方法必须标识所属的锁,而锁可以是任意对象,
任意对象都可以调用的方法必然是Object类中的方法,
因为所有类都是Object的直接或者间接子类。
2.生产者消费者问题
多线程中最常见的应用案例:生产者消费者问题。
生产和消费同时执行,需要多线程,
但是执行的任务却不相同,处理的资源确是相同的。
解决方法:多线程之间的通信,
生产者生产了商品后告知消费者来消费,这时的生产者应该处于等待状态,
消费者消费商品后告知生产者继续生产,这时的消费者应该处于等待状态。
(1)单生产者单消费者方式
生产一个消费一个
代码示例
/*
* 生产者消费者问题——单生产者单消费者
*/
public class ThreadDemo6 {
public static void main(String[] args) {
// 创建资源对象
Resource r = new Resource();
// 创建线程任务
Producer p = new Producer(r);
Customer c = new Customer(r);
// 创建线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
// 启动线程
t1.start();
t2.start();
}
}
/*
* 资源
*/
class Resource {
private String name;
private int count;
// 定义标记
private boolean flag = false;
public synchronized void put(String name) {
if (flag) {
// 线程等待
try {
wait();
} catch (Exception e) {
}
} else {
// 给成员变量赋值并加上编号
this.name = name + count;
// 编号自增
count++;
// 将标记改为true
flag = true;
// 输出生产了哪个商品
System.out.println(Thread.currentThread().getName() + "..生产者.."
+ this.name);
// 唤醒消费者
notify();
}
}
public synchronized void out(String name) {
if (!flag) {
// 线程等待
try {
wait();
} catch (Exception e) {
}
} else {
System.out.println(Thread.currentThread().getName() + "..消费者.."
+ this.name);
// 将标记改为false
flag = false;
// 唤醒生产者
notify();
}
}
}
/*
* 生产者
*/
class Producer implements Runnable {
Resource r;
// 生产者一初始化就要有资源
public Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.put("面包");
}
}
}
/*
* 消费者
*/
class Customer implements Runnable {
Resource r;
// 消费者一初始化就要有资源
public Customer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out("面包");
}
}
}
(2)多生产者多消费者方式
问题1:生产了商品没有被消费,
同一个商品被消费多次。
原因:
被唤醒的线程没有判断标记,导致问题1的产生。
解决:
只要让被唤醒的线程必须判断标记即可。将if改为while即可。
只要是多生产多消费,必须是while判断条件。
问题2:改成while循环后,死锁了
原因:生产方唤醒了线程池中生产方的线程。本方唤醒了本方。
解决方法:希望本方唤醒对方,没有对应的方法,所以只能唤醒所有 ,
虽然问题解决了,但是效率低了。
代码示例
/*
* 多生产多消费方式示例
*/
public class ThreadDemo7 {
public static void main(String[] args) {
// 创建资源对象
Resource r = new Resource();
// 创建线程任务
Producer p = new Producer(r);
Customer c = new Customer(r);
Producer p2 = new Producer(r);
Customer c2 = new Customer(r);
// 创建线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(p2);
Thread t4 = new Thread(c2);
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*
* 资源
*/
class Resource {
private String name;
private int count;
// 定义标记
private boolean flag = false;
public synchronized void put(String name) {
while (flag) {
// 线程等待
try {
wait();
} catch (Exception e) {
}
}
// 给成员变量赋值并加上编号
this.name = name + count;
// 编号自增
count++;
// 将标记改为true
flag = true;
// 输出生产了哪个商品
System.out.println(Thread.currentThread().getName() + "..生产者.."
+ this.name);
// 唤醒消费者
notifyAll();
}
public synchronized void out(String name) {
while (!flag) {
// 线程等待
try {
wait();
} catch (Exception e) {
}
}
System.out.println(Thread.currentThread().getName() + "..消费者.."
+ this.name);
// 将标记改为false
flag = false;
// 唤醒生产者
notifyAll();
}
}
/*
* 生产者
*/
class Producer implements Runnable {
Resource r;
// 生产者一初始化就要有资源
public Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.put("面包");
}
}
}
/*
* 消费者
*/
class Customer implements Runnable {
Resource r;
// 消费者一初始化就要有资源
public Customer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out("面包");
}
}
}
五 Lock和Condition-JDK1.5新特性
JDK 1.5以后提供了多生产多消费的解决方案
Lock接口:一个更加面向对象的锁,在该锁中提供了更多的显示的锁操作
lock():获取锁
unlock():释放锁
已经将旧锁替换成新锁,那么锁上的监视器方法(wait,notify,nptifyAll)也应该替换成新锁的监视器方法。
Condition接口:替换了Object对象中的监视器方法。
await():线程等待
signal():唤醒单个线程
signalAll():唤醒所有线程
JDK1.5提供的方式更加符合面向对象的特性,锁与监视器分离,
这种方式并没有解决JDK1.5以前的效率低问题,仅仅使用新对象改了写法而已。
老程序中可以通过两个锁嵌套完成,但是容易引发死锁,
新程序中,就可以解决这个问题:
只是用一个锁,锁只有一个,但是监视器对象可以有多组。
代码示例
/*
* 使用Lock接口和Condition接口解决多生产者多消费者问题
*/
public class Demo1 {
public static void main(String[] args) {
// 创建资源对象
Resource r = new Resource();
// 创建线程任务
Producer p = new Producer(r);
Customer c = new Customer(r);
Producer p2 = new Producer(r);
Customer c2 = new Customer(r);
// 创建线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(p2);
Thread t4 = new Thread(c2);
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*
* 资源
*/
class Resource {
private String name;
private int count;
// 定义一个锁对象
private Lock lock = new ReentrantLock();
//获取锁上的Condition对象
//为了解决本方唤醒对方的问题,一个锁上创建两个监视器对象
//获取生产者监视器对象
private Condition proCondition=lock.newCondition();
//获取消费者监视器对象
private Condition csmCondition=lock.newCondition();
// 定义标记
private boolean flag = false;
public void put(String name) {
// 获取锁
lock.lock();
try {
while (flag) {
// 线程等待
try {
proCondition.await();
} catch (InterruptedException e) {
}
}
// 给成员变量赋值并加上编号
this.name = name + count;
// 编号自增
count++;
// 将标记改为true
flag = true;
// 输出生产了哪个商品
System.out.println(Thread.currentThread().getName() + "..生产者.."
+ this.name);
// 唤醒消费者,且只需唤醒一个
csmCondition.signal();
} finally {
// 释放锁
lock.unlock();
}
}
public void out(String name) {
lock.lock();
try {
while (!flag) {
// 线程等待
try {
csmCondition.await();
} catch (Exception e) {
}
}
System.out.println(Thread.currentThread().getName() + "..消费者.."
+ this.name);
// 将标记改为false
flag = false;
// 唤醒生产者,且只需唤醒一个
proCondition.signal();
} finally {
lock.unlock();
}
}
}
/*
* 生产者
*/
class Producer implements Runnable {
Resource r;
// 生产者一初始化就要有资源
public Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.put("面包");
}
}
}
/*
* 消费者
*/
class Customer implements Runnable {
Resource r;
// 消费者一初始化就要有资源
public Customer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out("面包");
}
}
}
六 多线程的细节
1.sleep和wait方法的异同点
相同点:
可以让线程处于冻结状态。
不同点:
sleep必须指定时间,
wait可以指定时间也可以不指定时间。
sleep时间到,线程处于临时阻塞或者运行,
wait如果没有指定时间,必须要通过notify或者notifyAll唤醒。
sleep不一定非要定义在同步中,
wait必须定义在同步中。
当都定义在同步中时,
线程执行到sleep,不会释放锁,
线程执行到wait,会释放锁,
但是都释放CPU执行权。
2.线程如何停止
stop方法已经过时
线程结束就是让线程任务代码执行完,简而言之,就是让run方法结束
run方法中通常定义循环,只需控制住循环即可
注意:万一线程任务处于冻结状态,那么他将不能判断标记,
此时,应该使用interrupt方法来中断该等待,
所谓中断并不是停止线程,
interrupt的功能是将线程的冻结状态清除,让线程恢复到运行状态。(让线程重新具备CPU的执行资格)
因为是强制性的,所以会抛出一个异常,可以再catch中捕获异常,然后在异常处理中,改变标记让循环结束,使run方法结束。
代码示例
/*
* 线程停止演示
*/
public class StopThreadDemo {
public static void main(String[] args) {
Demo d=new Demo();
Thread t1=new Thread(d);
Thread t2=new Thread(d);
t1.start();
t2.start();
int x=0;
while(x<50){
x++;
System.out.println("mian---->"+x);
}
//修改标记,使线程任务结束,停止线程
d.stop();
System.out.println("over");
}
}
class Demo implements Runnable {
private boolean flag=true;
@Override
public void run() {
while(flag){
System.out.println(Thread.currentThread().getName()+"--->");
}
}
//修改标记使线程任务结束
public void stop(){
flag=false;
}
}
3.守护线程
守护线程即后台线程(一般创建的都是前台线程),
前台线程和后台线程运行时时一样的,都会获取CPU的执行权来执行,
只有结束的时候有些不同,
前台线程要通过run方法结束来使线程结束,
后台线程也可以通过run方法结束来使线程结束,但是还有另一种情况,
当进程中所有的前台线程都结束了,这时候无论后台线程处于什么样的
状态,都会结束,从而进程会结束。
进程结束依赖的都是前台线程。
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程,
当正在运行的线程都是守护线程时,Java虚拟机退出 ,
该方法必须在启动线程前调用。
4.线程的优先级
用数字标识 1-10
默认的初始优先级是5,最明显的3个优先级:1 5 10
public final void setPriority(int newPriority) 更改线程的优先级 。
5.线程组
线程组ThreadGroup
可以通过Thread的构造函数明确新线程对象所属的线程组。
线程组的好处:可以对多个同组线程进行统一的操作,
默认都属于main线程组。
6.join()和 yield()
yield()线程临时暂停,将执行权释放,让其他线程有机会获取执行权,
join():使当前线程等待调用该方法的线程停止。
代码示例
/*
* join() yield()代码示例
*/
class Demo1 implements Runnable {
@Override
public void run() {
for (int x = 1; x <= 20; x++) {
System.out.println(Thread.currentThread().getName() + "--->" + x);
}
}
}
public class JoinThreadDemo {
public static void main(String[] args) throws Exception {
Demo1 d = new Demo1();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
//线程等待t1线程终止
//主线程执行到这里,t1要加入执行,主线程就释放了执行权和执行资格并处于冻结状态,知道t1线程执行完才恢复
//一般用于临时加入一个运算的线程,让该线程执行完,程序才会执行
t1.join();
t2.start();
for (int x = 1; x <= 30; x++) {
System.out.println("mian---->" + x);
}
System.out.println("over");
}
}
七 案例
1.银行存款案例
案例:
两个客户到一个银行去存钱
每个客户一次存100,存3次
问题:
该程序是否存在安全问题
如果有,写出分析过程以及解决方案
产生问题:
多线程的随机性造成了安全问题的发生
问题分析:
1.既然是多线程的问题,必须问题发生在线程任务内。
2.在任务代码中是否有共性数据呢?
银行的金额(b对象中的sum)
3.是否对sum进行多次运算呢?
有
解决方式:
加同步
/*
* 测试类
*/
public class Test2 {
public static void main(String[] args) {
// 定义客户
Customer c = new Customer();
// 创建线程对象
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
// 启动线程
t1.start();
t2.start();
}
}
/*
* 银行类
*/
class Bank {
private int sum;
private Object obj = new Object();
public void add(int num) {
//对共享数据加同步
synchronized (obj) {
sum += num;
// 每存一次钱,看银行金额的变化
System.out.println("sum=" + sum);
}
}
}
/*
* 客户类,实现了Runnable接口
*/
class Customer implements Runnable {
//创建银行对象
private Bank b = new Bank();
@Override
public void run() {
for (int i = 0; i < 3; i++) {
// 一次存100,存3次
b.add(100);
}
}
}
2.懒汉式单例模式的安全问题
/*
* 饿汉式 多线程并发不会出现问题
*
* 单例懒汉模式的并发访问会出现安全性问题
* if (s == null) {
* s = new Single();
* }
* 这段代码中,如果多线程执行此段代码,s可能会被new 多次
*/
/*
* 懒汉式单例模式
*/
class Single {
private static Single s;
private Single() {
}
/*
* 并发访问会有安全隐患,所以加入同步机制解决安全问题
* 可以使用同步函数或者同步代码块解决此问题
* 但是,同步的出现降低了效率
* 可以通过双重判断的方式,解决效率问题,减少判断锁的次数
*/
public static/* synchronized */Single getInstance() {
/*
* 这种方式减少了判断同步的次数
*/
if (s == null) {
synchronized (Single.class) {
if (s == null) {
s = new Single();
}
}
}
return s;
}
}
class Demo implements Runnable {
/*
* 多线程任务里一般会写循环
* 如果对run()方法加了同步
* 很可能使其变成单线程
*/
@Override
public void run() {
Single.getInstance();
}
}
到此关于多线程的相关知识就整理完了。