文章目录
1、守护线程
2、线程的生命周期
3、线程的同步
1、问题描述
当多条语句操作同一个线程共享数据时,一个线程中的多条语句只执行了一部分,并没有执行完全,但此时另一个线程参与进来,导致共享数据的错误
也是因为这种原因,导致多线程具有了安全问题
2、解决办法
- 同步代码块/同步方法
- Lock接口
4、同步代码块与同步方法
对上图的一些解释:
操作共享数据的代码即需要被同步的代码
多个线程共同操作的对象即共享数据
同步监视器也称为锁,可以是任意类型的对象(类也是对象),但必须全局唯一
1、同步代码块相关
共享变量的选取:
- 任何对象都可以做共享数据,但通常情况下不会new一个对象来专门做共享变量
- 在实现Runnable接口时,由该Runnable接口创建的多个Thread均通过同一个Runnable实现类构造,因此实现类中定义的变量天然唯一,也就是可以通过this关键字来获得共享数据
- 当线程类是由继承Thread得到时,就不能使用this关键字了,因为多个线程创建的多个对象并非唯一对象,因此需要使用类对象做共享变量,即定义的Thread子类.class
2、同步方法相关
- 操作共享数据的代码完整地声明在一个方法中,可将该方法声明为同步方法
- 注意:
- 上面的代码可以将除while外的代码进行抽取作为新的方法,因此引出下一个问题
- synchronized声明在同步方法时,锁住的是this对象
-
如果通过Runnable接口创建的类,通过run方法或run方法内部调用其他同步方法时,是不会出现问题的,因为在上面提到过:Runnable实现类中定义的变量天然唯一,也就是可以通过this关键字来获得共享数据
-
但如果通过继承Thread类创建的类,是有多个对象的,因此需要给同步方法添加static,保证唯一性,也就是类锁
如果不加static,可以看到也是线程不安全的
-
5、单例模式【懒汉】优化
- 线程不安全的单例模式
public class Singleton {
}
//下面的类是一个懒汉式的单例模式,当且仅当该类getInstance()被调用时才实例化出唯一的类对象
class Bank {
private Bank() {
}
private Bank instance = null;
public Bank getInstance(){
if ( instance == null){
instance = new Bank();
}
return instance;
}
}
- 如果在实例化前有多个线程同时调用了getInstance(),就会出现线程不安全的情况
解决方法:加锁 - 方式一
//方式一:将获取类实例的方法设置为同步方法,将该类锁住,直到某线程调用完成方法,实例化出新的对象后才释放该锁
//这种方法同样会造成获取类实例时效率不高的问题,因为当类没有实例化前,多个线程抢占实例化的,进入方法后加锁的操作是合理的,可当对象实例化之后,对对象的获取也需要加锁显然会拖慢获取速度,因此需要进行优化
public static synchronized Bank getInstance(){
if ( instance == null){
instance = new Bank();
}
return instance;
}
- 方式二
// 方式二:同步代码块,效率较低,和方式一意思一样
// 最开始几个线程抢占初始化Bank实例对象,可实例化出对象后还需要加锁获取已经实例化出的对象,明显不合理
public static Bank getInstance(){
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
return instance;
}
}
- 方式三
//方式三:设置标志位,通过标志位判断是否加锁,避免后期加锁获取已有实例对象,效率较高
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
6、死锁
死锁举例
线程1进入run方法后,对s1对象进行加锁,进行相应操作后,为了使死锁现象出现,主动使其调用sleep(),给第二个线程充足的时间锁住s2,当另一个线程也锁住s2且该线程sleep()调用完毕后,尝试锁住s2,但s2此时已经被锁住,同样另一个线程也尝试锁s1,两个线程都在等待对方线程释放自己需要的资源,同时又不释放自己拥有的资源,因此造成死锁情况
public class DeadLock {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
System.out.println("成功锁住s1");
s1.append("a");
s2.append("1");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
}
System.out.println(s1);
System.out.println(s2);
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
synchronized (s2){
System.out.println("成功锁住s2");
s1.append("c");
s2.append("3");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
}
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}
运行结果:
6、Lock锁
- Lock介绍
- 代码演示
- Lock锁和synchronized关键字异同:
- Lock使用案例
代码实现
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
public void deposit(double money){
if (money>=0) {
balance += money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存钱,当前余额为:"+balance);
}
}
}
class AccountThread extends Thread{
private static ReentrantLock lock = new ReentrantLock();
private Account account;
public AccountThread(Account account) {
this.account = account;
}
@Override
public void run() {
try {
lock.lock();
for (int i=0;i<5;i++){
// 注意:这里也可以通过给Account中的deposit()添加synchronized关键字来解决线程安全问题
// 并且可以直接使用synchronized(this)来给当前account对象加锁,而在多个线程的run中直接调用该方法
// 前面虽然说通过继承的方法在编写同步代码块的时候不能直接通过this而要通过类名.class
// 进行同步,但它指的是如果synchronized声明在run()中也就是继承了Thread类的类中才不能直接使用this
// 本例中,同步块并没有定义在run中而是其他类中,因此还是唯一的
account.deposit(1000);
}
}finally {
lock.unlock();
}
}
}
- 代码测试
public class DepositMoneyTest {
public static void main(String[] args) {
Account account = new Account(0);
AccountThread t1 = new AccountThread(account);
AccountThread t2 = new AccountThread(account);
t1.setName("1");
t2.setName("2");
t1.start();
t2.start();
}
}
运行结果
7、线程的通信【wait与notify、notifyAll】
-
问题描述
-
代码实现
注意:notify默认情况下,其调用者为this
因此在同步代码块中锁定this对象时,不通过this.notify()
而直接使用notify()
也是可以的
而当同步对象为其他对象时,那么必须显式通过对象名.notify()
进行调用,否则会报错,详见下面代码
public class Number implements Runnable{
private int number = 1;
private Object object = new Object();
@Override
public void run() {
while (true){
synchronized (object){
// 唤醒当前阻塞的线程,并且这里调用的是this.notify()
object.notify();
// 假设当前线程1成功进入该同步代码块并执行相应方法,执行后 调用wait方法,被动阻塞,此时线程1无法再继续运行下去
// 调用wait方法会释放锁,因此线程2成功进入该同步块并唤醒1,而此时2拥有锁,因此1仍不能抢占锁资源
if (number<=100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": "+number);
number++;
try {
object.wait(); //执行完操作就将线程阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
public static void main(String[] args) {
Number number = new Number();
Thread thread1 = new Thread(number);
Thread thread2 = new Thread(number);
thread1.setName("1->");
thread2.setName("2->");
thread1.start();
thread2.start();
}
}
notify()会优先唤醒处于wait状态中优先级最高的线程
8、有关是否释放锁
综上:wait()、join()(底层也是wait())会释放锁,而sleep()、yield()不会释放锁
理解实现线程通信的三个方法均出自Object类:
- 调用者是同步监视器(三个方法的调用者均必须是同步代码块/同步方法中的同步监视器,且同步方法的默认同步监视器为调用它的对象)
- 同步监视器可以是任意对象
- 也就是说任意对象都可以调用这三种方法
- 因此wait()/notify()/notifyAll()均属于Object类中的方法
9、生产者、消费者问题
生产者线程内部调用店员类中定义的生产方法
消费者线程内部调用店员类中定义的消费方法
生产者线程和消费者线程均通过店员类进行创建,因此共享店员类中定义的商品数
10、JDK5.0新增创建线程方法
创建多线程的方法共有4种:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 使用线程池
1、Callable接口
1、基本介绍
2、通过Callable新建线程过程
- 创建实现Callable接口的实现类
- 实现call(),将此线程中需要实现的方法声明在其中
- 创建实现了Callable接口的类的实例对象callable
- 将callable作为构造器参数传入到FutureTask类中,创建FutureTask类实例对象futureTask
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
可以看出FutureTask类底层也实现了Runnable接口,因此可以作为参数传递到Thread类中进行线程的创建 - 若需要获取call()方法中的返回值,则需要通过调用futureTask.get()对其进行获取,不需要获取则不需要调用其get()
- 将futureTask作为参数传入Thread构造函数中,创建线程,通过调用start()开启线程执行相应call()中操作
代码实例:
package callable;/**
* @author zhihua.li
* @date 2021/3/23 - 10:48
**/
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
*@program: SE Reviewed
*@description:
*@author: zhihua li
*@create: 2021-03-23 10:48
*/
/*
* 1、创建实现Callable接口的实现类
* 2、实现call(),将此线程中需要实现的方法声明在其中
* 3、创建实现了Callable接口的类的实例对象callable
* 4、将callable作为构造器参数传入到FutureTask类中,创建FutureTask类实例对象futureTask
* public class FutureTask<V> implements RunnableFuture<V>
* public interface RunnableFuture<V> extends Runnable, Future<V>
* 可以看出FutureTask类底层也实现了Runnable接口,因此可以作为参数传递到Thread类中进行线程的创建
* 5、若需要获取call()方法中的返回值,则需要通过调用futureTask.get()对其进行获取,不需要获取则不需要调用其get()
* 6、将futureTask作为参数传入Thread构造函数中,创建线程,通过调用start()开启线程执行相应call()中操作
*/
public class CallableTest implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i%2==0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
public static void main(String[] args) {
CallableTest callable = new CallableTest();
FutureTask futureTask = new FutureTask(callable);
new Thread(futureTask).start();
try {
// get()的返回值即为futureTask构造器参数中实现了Callable接口的类对象重写的call()的返回值
Integer sum = (Integer) futureTask.get();
System.out.println("100内偶数总和为:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
3、Callable为什么比Runnable功能更加强大?
- call()可以有返回值
- call()可以抛出异常,并被外面的操作捕获,对调用者透明显示错误信息
- Callable支持泛型
2、线程池
1、相关介绍
2、代码实现
package threadPool;/**
* @author zhihua.li
* @date 2021/3/23 - 11:27
**/
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @program: SE Reviewed
* @description:
* @author: zhihua li
* @create: 2021-03-23 11:27
*/
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建一个包含10个线程的线程池,但此时并没有分配线程
ExecutorService service = Executors.newFixedThreadPool(10);
// 执行指定的线程操作,需要提供实现Runnable接口或Callable接口的实现类对象
service.execute(new NumberThread()); //打印偶数线程
service.execute(new NumberThread1()); //打印奇数线程
// service.submit(); //适用于Callable
service.shutdown(); //关闭线程池
}
}
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}