🏆作者简介,普修罗双战士,一直追求不断学习和成长,在技术的道路上持续探索和实践。
🏆多年互联网行业从业经验,历任核心研发工程师,项目技术负责人。
🎉欢迎 👍点赞?评论?收藏
Java注解知识专栏学习
Java并发知识云集 | 访问地址 | 备注 |
---|---|---|
Java并发知识点(1) | https://blog.csdn.net/m0_50308467/article/details/135216289 | Java并发专栏 |
Java并发知识点(2) | https://blog.csdn.net/m0_50308467/article/details/135260439 | Java并发专栏 |
Java并发知识点(3) | https://blog.csdn.net/m0_50308467/article/details/135301948 | Java并发专栏 |
Java并发知识点(4) | https://blog.csdn.net/m0_50308467/article/details/135357383 | Java并发专栏 |
线程之间可以通过以下方式进行通信:
在实际开发中,我们通常会使用共享内存、信号量、管道或消息队列来实现线程之间的通信。
在Java程序中,线程之间可以通过共享变量的方式来进行通信。线程之间共享变量时,需要注意多线程并发访问共享变量可能会导致数据不一致的问题,需要使用同步机制来保证共享变量的安全访问。
常用的同步机制有以下几种:
synchronized 关键字: 通过 synchronized 关键字来控制对共享变量的访问,保证在同一时刻只有一个线程可以访问共享变量。
wait()、notify()、notifyAll() 方法: 通过 Object 对象的 wait()、notify()、notifyAll() 方法来实现线程的等待、唤醒等操作。
Lock 接口: 使用 Lock 接口和 ReentrantLock 类可以实现更细粒度的同步控制。
这些同步机制可以保证线程之间共享变量的安全访问和互相协作。此外,Java 中还提供了一些高级的线程通信机制和工具,如 CountDownLatch、Semaphore、CyclicBarrier 等,这些工具可以方便地实现线程之间的控制和协作。
在 Java 中,可以通过以下方式创建守护线程:
daemon
参数设置为 true
。setDaemon()
方法中将 daemon
参数设置为 true
。以下是一个创建守护线程的示例:
public class DaemonThread extends Thread {
public DaemonThread() {
super("DaemonThread");
setDaemon(true);
}
@Override
public void run() {
while (true) {
System.out.println("I am a daemon thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
DaemonThread daemonThread = new DaemonThread();
daemonThread.start();
}
}
运行这个程序,会看到如下输出:
I am a daemon thread
I am a daemon thread
I am a daemon thread
...
可以看到,守护线程会一直运行,直到程序结束。
在 Java 中,可以通过以下方式确保线程安全:
synchronized
关键字。volatile
关键字。Lock
对象。以下是这三种方法的详细介绍:
synchronized
关键字synchronized
关键字可以保证在同一时间只有一个线程可以访问某个对象。如果多个线程同时访问一个 synchronized
对象,则只有一个线程可以获得该对象的锁,其他线程必须等待该线程释放锁之后才能获得锁。
以下是一个使用 synchronized
关键字来保证线程安全的示例:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在上述代码中, increment()
方法使用了 synchronized
关键字,因此在同一时间只有一个线程可以调用该方法。这保证了 count
变量的值不会被多个线程同时修改。
volatile
关键字volatile
关键字可以保证在同一时间所有线程都看到共享变量的最新值。如果多个线程同时访问一个 volatile
变量,则每个线程都会看到其他线程对该变量所做的最新修改。
以下是一个使用 volatile
关键字来保证线程安全的示例:
public class Counter {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在上述代码中, count
变量使用了 volatile
关键字,因此在同一时间所有线程都看到 count
变量的最新值。这保证了 count
变量的值不会被多个线程同时修改。
Lock
对象Lock
对象是一个更灵活的线程同步机制。 Lock
对象可以提供比 synchronized
关键字更细粒度的控制。
以下是一个使用 Lock
对象来保证线程安全的示例:
public class Counter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
在上述代码中, lock
变量是一个 Lock
对象。 increment()
方法使用 lock.lock()
方法获取 lock
对象的锁,然后使用 count++
方法将 count
变量的值加 1。最后,使用 lock.unlock()
方法释放 lock
对象的锁。
这三种方法都可以保证线程安全。在实际开发中,我们可以根据具体的情况选择合适的方法。
ReadWriteLock 是 Java 并发包中的一种锁,它可以让多个线程同时读取共享资源,但只能有一个线程可以写入共享资源。
ReadWriteLock 由两个锁组成:读锁和写锁。读锁允许多个线程同时读取共享资源,但不允许写入共享资源。写锁则只允许一个线程写入共享资源。
ReadWriteLock 可以用来实现读写分离,即读操作和写操作使用不同的锁。这样可以提高并发性,因为读操作不需要锁,因此可以同时进行。
ReadWriteLock 的使用方法如下:
以下是一个使用 ReadWriteLock 实现读写分离的示例:
public class ReadWriteLockDemo {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Map<String, String> map = new HashMap<>();
public String get(String key) {
readWriteLock.readLock().lock();
try {
return map.get(key);
} finally {
readWriteLock.readLock().unlock();
}
}
public void put(String key, String value) {
readWriteLock.writeLock().lock();
try {
map.put(key, value);
} finally {
readWriteLock.writeLock().unlock();
}
}
}
在上述代码中, readWriteLock
对象是一个 ReadWriteLock 对象。 get()
方法获取读锁,然后读取 map 中的值。 put()
方法获取写锁,然后将值写入 map 中。
ReadWriteLock 可以提高并发性,但它也有一定的缺点。例如,如果有多个线程同时获取读锁,那么这些线程可能会阻塞,直到第一个线程释放读锁。
在实际开发中,我们可以根据具体的情况选择是否使用 ReadWriteLock。如果读操作比较频繁,写操作比较少,那么可以使用 ReadWriteLock。如果读写操作都比较频繁,那么可以使用 synchronized 或 Lock。
volatile
变量和Atomic
变量(如AtomicInteger
、AtomicLong
等)都可以用于多线程环境下保证变量的可见性和原子性,但它们在实现机制和使用方式上有一些不同。
1. 可见性:volatile
变量保证了可见性,即一个线程对volatile
变量的修改对其他线程是可见的。而Atomic
变量也提供了可见性,但是更多是通过底层的内存屏障(memory barrier)和原子操作实现的。
2. 原子性:volatile
变量只能保证单个变量的读写操作是原子性的,但是对于复合操作(比如i++
这样的自增操作)无法保证原子性。而Atomic
变量则提供了一些原子操作的方法,如getAndIncrement()
、compareAndSet()
等,可以保证多个操作的原子性。
3. 使用方式:volatile
变量的使用比较简单,只需要声明为volatile
类型即可,对该变量的读写操作都会具有可见性。而Atomic
变量需要通过调用相应的原子操作方法来实现原子性操作。
总的来说,volatile
变量适用于只有简单的读写操作的场景,并且对于并发性能要求不高的情况下使用。而Atomic
变量则适用于对多个操作需要保证原子性的场景,可以方便地进行原子操作。
以下是volatile
变量和Atomic
变量的区别的表格说明:
特性 | volatile 变量 | Atomic 变量 |
---|---|---|
可见性 | 保证可见性 | 保证可见性 |
原子性 | 不保证原子性 | 提供一些原子操作方法,保证多个操作的原子性 |
使用方式 | 简单,只需声明为volatile 类型 | 通过调用原子操作方法实现原子性操作 |
并发性能要求 | 适用于并发性能要求不高的情况 | 适用于需要保证多个操作的原子性和高并发性能的情况 |
从上表可以看出,volatile
变量适用于简单的读写操作,并且对并发性能要求不高的情况下使用。它可以保证可见性,但不能保证复合操作的原子性。而Atomic
变量则提供了一些原子操作方法,能够保证多个操作的原子性,适用于需要高并发性能和原子性操作的场景。
可以,但是不建议直接调用 Thread 类的 run() 方法。
Thread 类的 run() 方法是一个普通的方法,它不会启动一个新的线程。如果直接调用 Thread 类的 run() 方法,那么这个方法会在当前线程中执行,而不是在一个新的线程中执行。
如果需要启动一个新的线程,可以使用 Thread 类的 start() 方法。Thread 类的 start() 方法会创建一个新的线程,并在该线程中执行 run() 方法。
以下是一个直接调用 Thread 类的 run() 方法的示例:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, world!");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();
}
}
在上述代码中,MyThread 类继承了 Thread 类,并重写了 run() 方法。run() 方法中打印了一条消息。
在 main() 方法中,创建了一个 MyThread 对象,并调用了它的 run() 方法。由于 MyThread 类继承了 Thread 类,因此它的 run() 方法实际上是调用了 Thread 类的 run() 方法。
由于 Thread 类的 run() 方法是一个普通的方法,它会在当前线程中执行,而不是在一个新的线程中执行。因此,在上述代码中,run() 方法会在 main() 方法所在的线程中执行,而不是在一个新的线程中执行。
如果我们希望在一个新的线程中执行 run() 方法,那么应该使用 Thread 类的 start() 方法。Thread 类的 start() 方法会创建一个新的线程,并在该线程中执行 run() 方法。
以下是一个使用 Thread 类的 start() 方法的示例:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, world!");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
在上述代码中,MyThread 类继承了 Thread 类,并重写了 run() 方法。run() 方法中打印了一条消息。
在 main() 方法中,创建了一个 MyThread 对象,并调用了它的 start() 方法。由于 Thread 类的 start() 方法会创建一个新的线程,因此 run() 方法会在一个新的线程中执行。
因此,使用 Thread 类的 start() 方法可以启动一个新的线程,并在该线程中执行 run() 方法。
有几种方法可以让正在运行的线程暂停一段时间。
Thread.sleep()
方法。 Thread.sleep()
方法可以让线程暂停指定的时间。Thread.yield()
方法。 Thread.yield()
方法可以让线程暂停,并让其他线程有机会运行。synchronized
关键字。 synchronized
关键字可以让线程在获取锁之前暂停。wait()
方法。 wait()
方法可以让线程在等待某个条件满足之前暂停。join()
方法。 join()
方法可以让线程等待另一个线程结束。在实际开发中,我们可以根据具体的情况选择合适的方法。
以下是几种让线程暂停一段时间的方法的示例代码:
1. 使用 Thread.sleep()
方法:
public class SleepExample {
public static void main(String[] args) {
System.out.println("Thread started");
try {
Thread.sleep(2000); // 暂停2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread resumed");
}
}
在上述代码中,使用 Thread.sleep(2000)
让线程暂停2秒。
2. 使用 Thread.yield()
方法:
public class YieldExample {
public static void main(String[] args) {
System.out.println("Thread 1 started");
Thread.yield(); // 暂停当前线程,让其他线程有机会运行
System.out.println("Thread 1 resumed");
System.out.println("Thread 2 started");
System.out.println("Thread 2 resumed");
}
}
在上述代码中,使用 Thread.yield()
让线程暂停,并让其他线程有机会运行。
3. 使用 synchronized
关键字:
public class SynchronizedExample {
public static void main(String[] args) {
System.out.println("Thread 1 started");
synchronized (SynchronizedExample.class) {
// 临界区代码
System.out.println("Thread 1 resumed");
}
System.out.println("Thread 2 started");
System.out.println("Thread 2 resumed");
}
}
在上述代码中,使用 synchronized
关键字让线程在获取锁之前暂停。
4. 使用 wait()
方法:
public class WaitExample {
public static void main(String[] args) {
final Object lock = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread 1 started");
lock.wait(); // 线程1等待
System.out.println("Thread 1 resumed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 2 started");
lock.notify(); // 唤醒线程1
System.out.println("Thread 2 resumed");
}
});
thread1.start();
thread2.start();
}
}
在上述代码中,使用 wait()
方法让线程在等待某个条件满足之前暂停,并使用 notify()
方法唤醒等待的线程。
5. 使用 join()
方法:
public class JoinExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 started");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 resumed");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 started");
try {
thread1.join(); // 等待线程1结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 resumed");
});
thread1.start();
thread2.start();
}
}
在上述代码中,使用 join()
方法让线程等待另一个线程结束,然后再继续执行。
以上示例代码展示了不同方法让线程暂停一段时间的方式。根据具体的需求,选择合适的方法来实现线程的暂停。
线程优先级是操作系统调度线程执行的相对重要性的指示。每个线程都可以具有不同的优先级,优先级较高的线程在调度时会更有可能被选择执行。
线程的优先级可以通过设置 Thread
类的 setPriority()
方法来指定,优先级范围从 1 到 10,其中 1 表示最低优先级,10 表示最高优先级。默认情况下,线程的优先级与创建它的父线程相同。
然而,需要注意的是,线程优先级仅是给操作系统的一个建议,操作系统可以根据自身的调度策略来决定是否遵循线程优先级。不同的操作系统可能对线程优先级的处理方式有所差异。
线程优先级的使用要谨慎,因为过分依赖线程优先级可能导致不可预测的结果,并且在不同的操作系统上可能表现不一致。在编写多线程应用程序时,更重要的是通过合理的设计和同步机制来确保正确的线程行为,而不是过度依赖线程优先级。
线程调度器(Thread Scheduler) 是操作系统中的一个组件,负责决定在多线程环境下,哪个线程获得 CPU 的执行时间。它根据一定的调度算法,将 CPU 时间划分成多个时间片,并按照一定的策略分配给各个线程,以实现多线程的并发执行。
时间分片(Time Slicing) 是线程调度器的一种策略,它将 CPU 时间划分为多个固定大小的时间片,每个时间片分配给一个线程执行。当一个线程的时间片用完后,线程调度器会暂停该线程的执行,并切换到下一个就绪的线程继续执行。这种轮流分配时间片的方式使得多个线程看起来是同时执行的。
时间分片 的优势是可以使多个线程共享 CPU 资源,实现并发执行,提高系统的吞吐量和响应性。它能够公平地分配 CPU 时间给各个线程,避免某个线程长时间占用 CPU 而导致其他线程无法执行的情况。
线程调度器和时间分片 是操作系统中实现多线程并发的重要机制。通过合理的调度算法和时间分片策略,线程调度器可以高效地管理多个线程的执行,使得多线程程序能够充分利用 CPU 资源,提高系统的性能和响应速度。
在 Java 中,可以使用 Thread.join()
方法来确保 main()
方法所在的线程是程序中最后结束的线程。 join()
方法会使当前线程等待调用该方法的线程执行完毕。
以下是一个示例代码,演示如何使用 join()
方法来确保 main()
方法所在的线程是程序中最后结束的线程:
public class MainThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 started");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 finished");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 started");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 finished");
});
thread1.start();
thread2.start();
// 等待 thread1 和 thread2 执行完毕
thread1.join();
thread2.join();
System.out.println("Main thread finished");
}
}
在上述代码中,创建了两个线程 thread1
和 thread2
,它们分别执行一些任务。在 main()
方法中,通过调用 join()
方法,使主线程等待 thread1
和 thread2
执行完毕。只有在这两个线程执行完毕后,主线程才会继续执行,输出 “Main thread finished”。
通过使用 join()
方法,可以确保 main()
方法所在的线程是程序中最后结束的线程。
线程通信的方法 wait()
, notify()
和 notifyAll()
被定义在 Object
类中是因为线程通信是基于对象的锁机制实现的。
在 Java 中,每个对象都有一个内部锁(也称为监视器锁或互斥锁),用于控制对对象的访问。当一个线程持有对象的锁时,其他线程需要等待该锁释放才能访问对象。
wait()
, notify()
和 notifyAll()
方法是与对象的锁紧密相关的。这些方法必须在持有对象的锁的情况下调用,否则会抛出 IllegalMonitorStateException
异常。
wait()
方法使当前线程进入等待状态,同时释放对象的锁,直到其他线程调用该对象的 notify()
或 notifyAll()
方法来唤醒等待的线程。notify()
方法唤醒在该对象上等待的一个线程,使其进入就绪状态,但不会立即释放对象的锁。notifyAll()
方法唤醒在该对象上等待的所有线程,使它们进入就绪状态,但不会立即释放对象的锁。由于每个对象都有这些基本的线程通信方法,因此它们被定义在 Object
类中,以便所有的对象都可以使用这些方法进行线程之间的通信。这种设计使得任何对象都可以作为线程间通信的锁和条件变量的持有者。
wait()
, notify()
和 notifyAll()
方法必须在同步方法或同步块中被调用,是因为这些方法涉及线程之间的协作和共享资源的同步。
在Java中,对象的监视器(通常是对象本身)用于实现线程之间的同步。同步方法和同步块使用监视器来保护共享资源的访问并确保线程的互斥执行。
在同步方法或同步块中调用wait()
方法会导致当前线程释放对监视器的持有,并进入阻塞状态,等待其他线程发出的通知。当其他线程通过notify()
或notifyAll()
方法唤醒了等待的线程后,被唤醒的线程才能重新获得监视器并继续执行。
如果不在同步方法或同步块中调用这些方法,将会抛出IllegalMonitorStateException
异常,因为这些方法的调用依赖于监视器的存在和有效的获取。
通过在同步方法或同步块中使用wait()
, notify()
和 notifyAll()
,可以确保线程之间正确地进行等待和唤醒,并且保护共享资源的一致性和正确性。同步机制使得线程之间能够有序地进行协作,避免了竞态条件和数据不一致的问题。
Thread
类的sleep()
方法和yield()
方法是静态的,是因为它们不是针对特定线程对象的操作,而是作用于正在执行的当前线程或当前线程所属的线程组。
sleep()
方法:线程通过Thread.sleep(long millis)
方法在指定的时间内暂停执行,进入阻塞状态。这个方法是静态的,因为它不会直接操作线程对象,而是让当前正在执行的线程暂停执行。可以使用Thread.sleep()
方法模拟定时任务、线程调度等需求。
yield()
方法:线程通过Thread.yield()
方法告诉调度器当前线程可以放弃当前CPU时间片,将自己重新放回就绪状态,让其他线程有机会执行。这个方法是静态的,因为它不会直接操作线程对象,而是操作当前线程的调度状态。
由于sleep()
和yield()
方法不依赖于特定的线程对象,所以将它们定义为静态方法更为合理。通过静态方法的方式,可以直接通过Thread.sleep()
和Thread.yield()
调用这些方法,无需先创建Thread
对象。
需要注意的是,虽然这些方法是静态的,但它们仍然会影响到调用它们的当前线程。因此,它们的使用需要谨慎,需要在适当的时机和场景下使用,并考虑线程安全和协调性。
同步方法和同步块都可以用于实现线程安全和共享资源的同步,它们各自有不同的适用场景和优缺点。
同步方法:
synchronized
关键字,无需显式地获取和释放锁,编码更加简洁。同时,同步方法可以保证整个方法的执行过程是原子性的,即线程在执行同步方法期间,其他线程无法进入该方法。同步块:
Lock
接口的实现类提供更细粒度的锁控制。综上所述,选择使用同步方法还是同步块取决于具体的场景和需求。通常情况下,推荐使用同步方法,因为它简单、易于理解和实现。但对于需要更细粒度控制的同步需求,或者需要使用多个锁对象来提高并发性能的情况下,同步块更适合。同时,还可以考虑使用Lock
接口的实现类提供更高级的锁定机制。
java.util.Timer
类是Java中的一个定时器工具,用于安排在将来的指定时间执行任务。它可以用来定期执行任务,或者在指定的时间点执行任务。
要创建一个具有特定时间间隔的任务,可以按照以下步骤进行操作:
1. 创建一个继承自java.util.TimerTask
的任务类,并实现run()
方法,在该方法中定义要执行的任务逻辑。
import java.util.TimerTask;
public class MyTask extends TimerTask {
public void run() {
// 执行任务的逻辑
}
}
2. 创建一个java.util.Timer
对象。
import java.util.Timer;
Timer timer = new Timer();
3. 使用schedule()
方法安排任务的执行。 该方法有多个重载形式,可以根据需求选择适合的方法。
以下是一种常用的方式,创建一个每隔一定时间执行一次的任务:
MyTask task = new MyTask();
long delay = 0; // 初始延迟时间(单位:毫秒)
long period = 1000; // 执行间隔时间(单位:毫秒)
timer.schedule(task, delay, period);
上述代码将会创建一个定时任务,初始延迟0毫秒后开始执行,然后每隔1000毫秒执行一次。
需要注意的是,java.util.Timer
类是单线程的,如果一个任务执行的时间过长,可能会影响后续任务的执行。另外,如果任务抛出未捕获的异常,整个定时器将会停止运行。
为了更好地应对复杂的定时任务需求,Java 5开始引入了更强大的ScheduledExecutorService
接口,通常被认为是java.util.Timer
的替代品。它提供了更灵活的执行方式和更好的错误处理机制。