一文详细聊聊java的多线程
作者:晔子yy
1.什么是多线程
定义:多线程是指在一个程序中同时执行多个线程的技术。每个线程代表一个独立的执行路径,但共享相同的内存空间和系统资源。
优势:
提高CPU利用率 :当一个线程等待I/O操作时,另一个线程可以继续执行
改善响应性 :用户界面保持响应,后台处理任务
并发处理任务 : 同时处理多个请求或计算
平时我们使用的大多都是单线程也就是main线程,虽然已经可以完成大部分的工作,但是如果任务量一旦多起来,那么你程序的吞吐量或许会指数型下降。这时候,能多点“帮手”一起完成任务就是至关重要的了,接下来我们来说下如何创建多线程
2.多线程的常见实现方式
2.1 继承Thread类
这是最经典也是最简单的实现方式,只需要自定义类继承java.lang.Thread类,重写其run()方法,run()方法中定义了线程执行的具体任务。创建该类的实例后,通过调用start()方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
}
}
// 使用
MyThread thread1 = new MyThread();
thread1.start(); // 启动线程使用Thread虽然简单,但是有个非常显而易见的缺点:由于java只支持单继承,所以MyThread这个类不能再继承其他的父类。
2.2实现Runnable接口
实现Runnable接口也可以创建多线程,并且没有继承Thread类的缺点,这也是开发中推荐使用的多线程实现的方式之一
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
}
}
// 使用
Thread thread2 = new Thread(new MyRunnable());
thread2.start();2.3实现Callable接口与Future
java.util.concurrent.Callable接口类似于Runnable,不同点在于Callable的call()方法可以有返回值并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接口。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码,这里返回一个整型结果
return 1;
}
}
public static void main(String[] args) {
MyCallable task = new MyCallable();
//使用FutureTask包装
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();
try {
Integer result = futureTask.get(); // 获取线程执行结果
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}可以看到,使用这种方法编程稍微有些复杂,所以我更推荐平时使用第二种方式去开启线程
3.线程的并发安全问题
3.1问题抛出
在jvm的内存结构中,多线程之间有一块共享的区域是堆内存,该区域经常存放对象、数组等,简单来说,平时new出来的对象大部分都放在了堆内存中,此时如果我们随意地使用多线程就会引发一个严重的问题——线程并发安全问题。
举个简单的例子:有100张票,3个线程去分,结果会如何?
//开启三个线程
Thread thread1 = new Mythread();
Thread thread2 = new Mythread();
Thread thread3 = new Mythread();
thread1.start();
thread2.start();
thread3.start();
//主线程休眠1秒
Thread.sleep(1000);
System.out.println("执行了"+ticket.count+"次");
我们可以清楚的观察到不仅票的数量不是递减的,总执行的次数也对不上,这就是多线程环境下的并发安全问题。
3.2解析问题
问题的根本就是ticket是全局共享的,对于对象成员变量的修改,线程会先拿到值后再做修改,由于这两步并不是同时进行的,所以会导致在一个线程做修改之前,另一个线程拿到了修改前的值,比如t1先取100,在做-1之前,t2也取到了100,两个线程先后做-1操作,此时t3来拿值,取到了98,不仅少了99这个状态,票数100也出现了两次,在打印的时候,由于各个线程顺序的不确定性,也会出现后打印的票数比前打印的票数多的情况。
那怎么解决问题呢?
3.3 synchronized关键字
定义:Java语言的关键字,用于实现线程同步。当修饰方法或代码块时,同一时间仅允许一个线程执行该同步区域,其他线程需等待当前线程释放锁。
synchronized就像是一把锁,当一个线程想要执行被synchronized修饰的方法时,就必须先拿到锁才能执行,之后又有线程想执行该方法后就会被阻塞,直到锁被释放才能去竞争锁,竞争成功后就可以执行方法
synchronized有三种实现方法:
1.同步实例方法
public class BankAccount {
private int balance = 1000;
// 锁住当前账户对象(this)
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
}由于锁的是同步方法,所以实际上是对这个对象(this)进行了上锁,这就导致只有在多线程一起使用这个对象的时候才可以实现数据隔离,如果又new了一个BankAccount对象,这时候就不能实现隔离,举个简单的例子,把这个对象当作是一间房子,锁同步方法仅仅只能防止别人进你家,不能防止别人进其他人家。
2.同步静态方法
public class BankAccount {
private int balance = 1000;
// 锁住当前账户对象(this)
public static synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
}static修饰后的方法就是静态方法,生命周期上升到类的级别,与类强绑定,这时候使用synchronized修饰后相当于锁住了整个类,由于类是全局唯一的,所以就解决了创建多对象后无法实现数据隔离的情况了,再拿刚刚例子来说,这次别人既进不来你家,也进不去其他的房子里。万事大吉了!
3.同步代码块
public class BankAccount {
private int balance = 1000;
private final Object lock = new Object(); // 专门的锁对象
// 锁住当前账户对象(this)
public void withdraw(int amount) {
synchronized(lock) { // 使用专门的锁对象
if (balance >= amount) {
balance -= amount;
}
}
}
}public class BankAccount {
private int balance = 1000;
// 锁住当前账户对象(this)
public void withdraw(int amount) {
synchronized(Object.class) { // 使用全局的类上锁
if (balance >= amount) {
balance -= amount;
}
}
}
}相比于前两种,这一种方法可以做到锁的粒度更细,性能会有所提升,在synchronized()中,你既可以模拟第一种方法锁住对象,也可以模拟第二种方法使用类去全局上锁,两者效果均不变。
3.4解决问题
好了,我们已经大概了解了synchronized关键字的使用,接下来就是解决遗留的问题了,方法很简单,直接在buyTicket()前使用synchronized关键字修饰一下即可。
public synchronized static void buyTicket() {
ticketid--;
System.out.println(Thread.currentThread().getName() + "买了票,现在还剩下" + ticketid + "张");
count++;
}加上sychronized后我们再来查看结果

现在无论我们执行多少次,结果都不会出问题了。
总结
到此这篇关于java多线程的文章就介绍到这了,更多相关java多线程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
