1、创建线程的方式:
继承Thread类
实现Runable接口
1、继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
// 执行线程任务
System.out.println("Thread: Hello, world!");
}
}
// 在主线程中启动新线程
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2、实现Runable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
// 执行线程任务
System.out.println("Runnable: Hello, world!");
}
}
// 在主线程中启动新线程
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable(); // 创建 Runnable 对象
Thread thread = new Thread(myRunnable); // 创建线程,传入 Runnable 对象
thread.start(); // 启动线程
}
}
区别:
1、实现 Runnable 接口更能解耦的原因是因为它将任务的执行逻辑与任务的创建和调度逻辑分离开来。当一个类实现了 Runnable 接口,它只需要实现 run 方法来定义任务的具体执行逻辑。这样,任务的创建和调度可以由其他代码来完成,类之间的依赖性会减少。
2、java是单继承,推荐使用Runable在实现接口的同时可以继承
2、竞态条件
竞态条件(Race condition)指的是当多个线程同时访问和修改共享资源时,由于执行顺序不确定
或者不同线程之间缺乏同步机制
,可能会导致结果出现不确定性或不正确的情况。
案例:线程不安全之售票
public class Ticket implements Runnable {
private static int total = 100;
@Override
public void run() {
//获取当前窗口线程
Thread window = Thread.currentThread();
while (true) {
if (total > 0) {
/**
* 模拟支付、出票用时(可能出现情况:最后一张票还在出票的过程中,另外两个线程也进入到if判断)
* 另外两个线程并不知道这个张即将被售出,所以也会同样去出售,最后导致的结果可能会超卖两张票
*/
try {
/**
* 父类方法并没有抛出异常,所以子类只能try-catch捕获异常处理
*/
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(e);
}
total--;
System.out.println(window.getName() + "售票成功,票数剩余:" + total);
}else{
break;//如果票售光就退出循环
}
}
}
public class Main {
public static void main(String[] args) {
tickect ticket = new tickect();
for (int i = 1; i <= 10; i++) {
new Thread(ticket, "窗口" + i).start();
}
}
}
}
为了避免竞态条件问题,我们需要采用同步机制(比如synchronized关键字)来保证多个线程对共享资源的互斥访问。在本例中,我们可以使用synchronized来修饰run()方法,这将确保在任何时候只有一个线程能够访问run()方法,从而避免了竞态条件问题。
3、synchronized关键字和其锁定的对象
public class UnsafeBank {
public static void main(String[] args) {
Account account=new Account(100,"结婚基金");
Arawing you=new Arawing(account,50,"你");
Arawing love=new Arawing(account,70,"妻子");
you.start();
love.start();
}
}
public class Account {
int money;//余额
String name;//账户
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
public class Arawing extends Thread {
private static Account account;//账户
private int arawingMoney;
private int nowMoney=0;
private String name;
public Arawing(Account accout,int arawingMoney,String name){
super(name);
this.account=accout;
this.arawingMoney=arawingMoney;
}
@Override
public void run() {
//银行共享账户资源 对账户加上同步
synchronized(account) {
if (account.money - arawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money = account.money - arawingMoney;
nowMoney = nowMoney + arawingMoney;
}
//System.out.println(Thread.currentThread().getName()+"取了"+arawingMoney);
System.out.println(this.getName()+"手上有"+nowMoney);
System.out.println("余额"+account.money);
}
}
1、为什么锁定accout对象?
在这个示例中,synchronized(account)语句锁定的是Account对象,当某个线程运行到synchronized(account)时,会尝试获取account对象的锁,如果锁已被其他线程占用,则该线程会等待,直到其他线程释放account的锁。当该线程获取到锁后,才能进入到同步块中执行代码。这样可以保证,同一时间只有一个线程能够访问account对象,从而避免了数据竞争和线程安全问题
2、可以锁定this?
如果你创建了多个Arawing对象,每个对象上的同步代码块是互不干扰的,也就是说不同的线程可以同时执行不同对象上的同步代码块。因此,要根据具体的需求来选择合适的锁对象。在这个例子中,锁定account对象更符合共享资源的粒度,锁定this可能会导致并发性能降低,因为每个Arawing对象
都有自己的锁。综上,需要锁定的对象是所有线程共享
的同一对象
3、死锁
import java.util.concurrent.TimeUnit;
public class DeadLockTest {
public static void main(String[] args) {
String itemA = "itemA";
String itemB = "itemB";
// 创建两个线程,分别持有itemA和itemB两个资源,并尝试获取对方持有的资源
new Thread(new MyThread(itemA,itemB),"Thread01").start(); // 持有itemA资源,想拿itemB资源
new Thread(new MyThread(itemB,itemA),"Thread02").start(); // 持有itemB资源,想拿itemA资源
}
}
class MyThread implements Runnable {
private String itemA;
private String itemB;
public MyThread(String itemA, String itemB) {
this.itemA = itemA;
this.itemB = itemB;
}
@Override
public void run() {
synchronized (itemA) {
System.out.println(Thread.currentThread().getName() + " item: " + itemA + " => get " + itemB);
// 休眠2秒
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (itemB) {
System.out.println(Thread.currentThread().getName() + " item: " + itemB + " => get " + itemA);
}
}
}
}
在DeadLockTest类中,创建了两个线程Thread01和Thread02,分别持有itemA和itemB两个资源,并尝试获取对方持有的资源。这种情况下,如果线程Thread01先获得了itemA资源,而线程Thread02先获得了itemB资源,那么它们在尝试获取对方持有的资源时,就会相互等待对方释放资源,进入死锁状态
1、避免使用多个锁,尽量只使用一个锁来管理资源访问。
2、统一获取锁的顺序,即所有线程都按照相同的顺序获取锁。
3、避免嵌套锁。
4、使用tryLock()等非阻塞的锁获取方法,及时释放锁资源,避免长时间持有锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
if (lock.tryLock()) {
try {
// 成功获取锁,执行需要同步的代码块
System.out.println("获取到锁,开始执行任务...");
// 模拟任务执行
Thread.sleep(1000);
System.out.println("任务执行完毕。");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("未获取到锁,执行其他逻辑...");
}
}
}
在这个示例中,如果锁已经被其他线程持有,则tryLock()方法会返回false,程序会跳过同步代码块的执行,直接执行后续的逻辑。这样可以避免长时间等待锁而导致线程阻塞,提高了程序的响应性。
根据哈希码来决定加锁顺序,保证不同线程获取资源的顺序一致,避免死锁问题
class MyThread implements Runnable {
private String itemA;
private String itemB;
public MyThread(String itemA, String itemB) {
this.itemA = itemA;
this.itemB = itemB;
}
@Override
public void run() {
// 获取两个资源的哈希码,用于确定加锁顺序
int hashA = System.identityHashCode(itemA);
int hashB = System.identityHashCode(itemB);
// 根据资源的哈希码来决定加锁顺序,保证获取资源的顺序一致
if (hashA < hashB) {
synchronized (itemA) {
System.out.println(Thread.currentThread().getName() + " item: " + itemA + " => get " + itemB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (itemB) {
System.out.println(Thread.currentThread().getName() + " item: " + itemB + " => get " + itemA);
}
}
} else if (hashA > hashB) {
synchronized (itemB) {
System.out.println(Thread.currentThread().getName() + " item: " + itemB + " => get " + itemA);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (itemA) {
System.out.println(Thread.currentThread().getName() + " item: " + itemA + " => get " + itemB);
}
}
} else {
// 哈希码相同,需要进行其他方式的判断或处理
System.out.println("哈希码相同,请进行其他处理");
}
}
}
延申思考:上述可以采用可重入锁的方式替代hash顺序的方式吗?
可重入锁(Reentrant Lock)确实主要解决了同一个线程可以多次获取同一把锁的情况,但并不能直接解决死锁问题。死锁是指一组
线程在相互等待对方释放资源的情况下导致所有线程都无法继续执行的情况。可重入锁虽然可以避免同一个
线程重复获取同一把锁时产生死锁,但并不能完全解决多个
线程之间的复杂交互导致的死锁问题。
4、线程通信
线程通信是指多个线程之间进行协调和交互的过程
通过使用线程的协作和通信机制,如线程的等待和唤醒(wait()、notify()、join()等),可以实现线程的同步和互斥访问共享资源
,以及线程之间的信息交换。这样一来,我们可以在需要的时候等待其他线程的完成,以保证线程的执行顺序和结果的一致性
当调用 join() 方法时,当前线程会尝试获取被调用
线程的锁,如果获取成功,则会阻塞当前
线程,并等待被调用线程执行完成释放锁(让被调用
线程参加进来)
调用 join() 方法时可以提供一个超时参数,指定等待的最长时间。如果超过了指定的超时时间,即使被调用的线程尚未执行完成,调用 join() 方法的线程也会从阻塞状态返回,并继续执行后续代码
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("子线程开始执行");
try {
Thread.sleep(5000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程执行完成");
});
thread.start();
System.out.println("主线程等待子线程执行完成");
// 调用 join() 方法并指定等待的最长时间为3秒
// 这里是主线程即获取锁的线程等待调用join方法的线程执行的时间
thread.join(3000);
if (thread.isAlive()) {
// 如果子线程超过3秒还未执行完成则返回true
System.out.println("等待超时,继续执行主线程");
} else {
// 如果子线程已执行完毕,则 join() 方法会立即返回
System.out.println("子线程执行完成");
}
System.out.println("主线程执行完毕");
}
}
根据您提供的代码,主线程等待子线程执行完成后会打印一些提示信息。根据 join()
方法的返回结果,会打印不同的消息。以下是可能的输出结果:
- 如果子线程在 3 秒内执行完成:
主线程等待子线程执行完成
子线程开始执行
子线程执行完成
主线程执行完毕
- 如果子线程超过 3 秒仍在执行:
主线程等待子线程执行完成
子线程开始执行
等待超时,继续执行主线程
主线程执行完毕
在第一种情况下,子线程能够在指定的时间范围内完成,join()
方法会立即返回,然后打印子线程执行完成的信息。
在第二种情况下,子线程超过指定的时间仍未完成,join()
方法会返回,然后打印等待超时的信息,接着主线程继续执行后续代码,最后打印主线程执行完毕的信息。
需要理解的2个问题:
1、thread.join(3000)
这里谁是当前线程,谁是被调用线程?
thread 是被调用线程,当前的线程是主线程;thread里重写的run方法才是属于它自己的线程要执行的逻辑
2、join保证执行结果的顺序性?
多个线程可以并发地执行,它们可能同时读写共享的数据或访问共享的资源。这样就会出现数据不一致。为了保证程序的正确性,需要确保线程的执行顺序。线程使用 join() 方法等待子线程的完成。这样就保证了主线程在子线程执行完成之后再输出结果,确保了执行结果顺序性。
当你向面试官介绍 join()
方法时,你需要详细说明它的作用、使用规则以及在实际应用中的场景。
总结:
-
作用:
join()
方法是 Java 中的一个线程方法,它允许一个线程等待另一个线程的结束。简而言之,调用join()
方法的线程会等待被调用线程执行完毕后再继续执行。 -
使用规则:在需要等待其他线程执行完毕后再继续执行的地方调用
join()
方法即可。该方法会使当前线程进入等待状态,直到被调用线程执行完毕或指定的等待时间到期。 -
应用场景:
- 等待子线程执行完毕后再执行主线程中的后续逻辑。
- 协调多个线程的执行顺序,确保某些线程在其他线程执行完毕后才能开始执行。
- 控制线程之间的依赖关系,例如,主线程需要等待多个子线程执行完特定任务后才能继续执行,这时就可以使用
join()
方法。
举例来说,假设有一个主线程和三个子线程,主线程需要等待这三个子线程全部执行完毕后才能执行某些操作。你可以在主线程中依次调用每个子线程的 join()
方法,确保主线程等待所有子线程执行完毕后再继续执行。
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable("Thread 1"));
Thread t2 = new Thread(new MyRunnable("Thread 2"));
Thread t3 = new Thread(new MyRunnable("Thread 3"));
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All threads have finished execution. Proceeding with main thread.");
}
}
class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Thread " + name + " is executing.");
try {
Thread.sleep(2000); // Simulating some task
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + name + " has finished execution.");
}
}
在这个例子中,主线程需要等待三个子线程(t1
、t2
、t3
)执行完毕后再继续执行,所以使用了 join()
方法来实现这个需求。
notifyAll()
和 wait()
这两个方法是 Java 中用于线程通信的两个重要方法。简单来说,notifyAll()
方法用于通知等待该对象锁的所有线程,而 wait()
方法则用于暂停当前线程的执行,让出该对象的锁并进入等待队列,直到被唤醒。
class WaitNotifyExample {
// final 修饰符可以确保在多个线程之间操作 lock 的一致性
private final Object lock = new Object();
private boolean flag = false;
public void waitMethod() {
synchronized (lock) {
while (!flag) {
try {
lock.wait(); // 等待通知(阻塞)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("waitMethod: flag is true now.");
}
}
public void notifyMethod() {
synchronized (lock) {
flag = true;
lock.notifyAll(); // 唤醒所有该对象锁的线程
System.out.println("notifyMethod: lock notified.");
}
}
}
调用
WaitNotifyExample example = new WaitNotifyExample();
Thread waitThread1 = new Thread(example::waitMethod);
Thread waitThread2 = new Thread(example::waitMethod);
Thread notifyThread = new Thread(example::notifyMethod);
waitThread1.start();
waitThread2.start();
// 等待 1 秒,让 waitThread1 和 waitThread2 先执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyThread.start();
在 main()
方法中,我们启动了两个等待线程 waitThread1
和 waitThread2
,以及一个通知线程 notifyThread
。由于 waitMethod()
方法中使用了 wait()
方法等待通知线程的通知,为了确保等待线程能够正确地接收到通知,我们在启动通知线程之前等待了 1 秒钟。
可以看到,在 notifyAll()
方法被调用后,两个等待线程都被唤醒并继续执行,输出了 "waitMethod: flag is true now.",表明线程成功地接收到了通知,实现了线程间的通信和协同。
notifyMethod: lock notified.
waitMethod: flag is true now.
waitMethod: flag is true now.
思考:以下代码正确吗?为什么?
class WaitNotifyExample {
// final 修饰符可以确保在多个线程之间操作 lock 的一致性
private final Object lock = new Object();
public void waitMethod() {
synchronized (lock) {
try {
lock.wait(); // 等待通知(阻塞)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("waitMethod: flag is true now.");
}
}
public void notifyMethod() {
synchronized (lock) {
lock.notifyAll(); // 唤醒所有该对象锁的线程
System.out.println("notifyMethod: lock notified.");
}
}
}
不正确,在多线程环境下,线程有可能因为一些其他原因被唤醒(虚拟唤醒),即使没有调用 notifyAll() 方法。因此,为了避免虚假唤醒,应该始终在循环中
使用 wait() 方法。
5、竞态条件1++
需要理解的
1++实际是几个步骤?
- 首先,取出变量的当前值。在这个例子中,变量的当前值是1
- 然后,将变量的值加1。在这个例子中,1会加1,得到2
- 最后,将新的值写回到变量中。在这个例子中,将2写回到变量中
int x = 0;
public void run() {
for(int i=0; i<100000; i++) {
x++;
}
}
public void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
public void run() {
for(int i=0; i<100000; i++) {
x++;
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
for(int i=0; i<100000; i++) {
x++;
}
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final value of x: " + x);
}
使用join让主线程最后执行
说明:在给定的代码中,通过使用join方法,主线程会等待t1和t2线程完成后再继续执行。这是为了确保在打印最终结果之前,两个线程都已经完成自增操作
结果可能因线程之间的竞争条件而有所不同。运行多次后可以看到,最终 x 的值通常小于200000
使用悲观锁保证自增的正确性
public class Main {
private int x = 0;
private final Object lock = new Object();
public void incrementX() {
synchronized (lock) {
x++;
}
}
public static void main(String[] args) {
Main main = new Main();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
main.incrementX();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
main.incrementX();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final value of x: " + main.x);
}
}
6、管程法
什么是管程法?就是并发协作模型“生产者/消费者模式”实现方式的一种。
思想:
1.首先需要四个角色 :1.生产者2.消费者3.缓冲区4.馒头
2.生产者生产馒头放到缓冲区,缓冲区如果满了,生产者停止运作,进入等待
3.消费者从缓冲区拿馒头,如果缓冲区馒头没有了,先唤醒生产者,然后进入等待
import java.util.LinkedList;
import java.util.Queue;
public class MonitorDemo {
// 缓冲区大小
private static final int BUFFER_SIZE = 10;
// 管程对象
private final Object mLock = new Object();
// 缓冲区
private final Queue<Integer> mBuffer = new LinkedList<>();
// 生产方法
public void produce() {
synchronized (mLock) {
// 当缓冲区已满时,线程进入等待状态
while (mBuffer.size() == BUFFER_SIZE) {
try {
mLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产一个随机数
int value = (int) (Math.random() * 100);
// 添加到缓冲区
mBuffer.add(value);
// 输出生产信息
System.out.println(Thread.currentThread().getName() + " produced: " + value);
//唤醒其他等待线程
mLock.notifyAll();
}
}
// 消费方法
public void consume() {
synchronized (mLock) {
// 当缓冲区为空时,线程进入等待状态
while (mBuffer.isEmpty()) {
try {
mLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从缓冲区中取出一个元素
int value = mBuffer.remove();
// 输出消费信息
System.out.println(Thread.currentThread().getName() + " consumed: " + value);
// 唤醒其他等待线程
mLock.notifyAll();
}
}
public static void main(String[] args) {
// 创建 MonitorDemo 对象
MonitorDemo monitorDemo = new MonitorDemo();
// 创建生产者线程
Thread producer = new Thread(() -> {
for (int i = 1; i <= 20; i++) {
monitorDemo.produce();
}
});
// 创建消费者线程
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 20; i++) {
monitorDemo.consume();
}
});
// 启动线程
producer.start();
consumer.start();
}
}
管程法通过引入互斥锁(mutex)和条件变量(condition variables)来实现线程之间的同步和通信。互斥锁用于保护共享资源,确保同一时间只有一个线程可以访问共享资源。而条件变量用于线程之间的等待和通知,一个线程可以等待一个条件变量的特定条件成立,而另一个线程可以通过发送信号通知等待线程