目录:
1. 线程的创建方式
2. 继承Thread类
3. 实现Runnable接口
4. 一些注意的细节
5. 区分run()和start()
6. Java虚拟机启动时是单线程还是多线程的?
7. 推荐使用哪种方式创建线程?
8. 线程的优点
9. Thread类及其常用方法
10. 线程的几种状态
11. 理解线程的几种状态:
12. 线程安全
13. 线程安全的概念:
14. 线程不安全的原因:
15. 原子性:
16. 可见性
17. 代码顺序性
18. 解决线程不安全
19. synchronized关键字—监视锁monitor lock
20. volatile 关键字
21. 线程之间的通信
22. **wait() 与 notify() 和 notifyAll()**
23. 单例模式
24. 立即加载-饿汉式
25. 延迟加载-懒汉模式
26. 双重校验锁(DCL)
27. 面试题
28. 进程与线程的区别?线程死了进程会死吗?
29. volatile和synchronized区别
30. sleep和wait分别是那个类的方法,有什么区别
31. 线程cpu进行资源调度和分配的基本单位,线程间共享进程的内存,一个进程至少一个线程
32. 线程的创建方式
33. 继承Thread类
34. 实现Runnable接口
35. 一些注意的细节
36. 区分run()和start()
37. Java虚拟机启动时是单线程还是多线程的?
38. 推荐使用哪种方式创建线程?
39. 线程的优点
实现多线程:
1. 创建实现Runnable接口的类,例如MyRunnable:
```java
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
```
2. 在ThreadTest类的main方法中,创建MyRunnable对象,并将其作为参数传递给Thread类的构造器,创建Thread类的对象。然后通过Thread类的对象来启动线程,调用run()方法:
```java
public class ThreadTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable); // 将MyRunnable对象作为参数传递给Thread类的构造器
Thread thread2 = new Thread(myRunnable); // 将MyRunnable对象作为参数传递给Thread类的构造器
thread1.start(); // 启动线程1
thread2.start(); // 启动线程2
}
}
```
以下是重构后的代码:
```java
class MThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MThread t = new MThread();
Thread thread = new Thread(t);
Thread thread2 = new Thread(t);
thread.setName("线程1");
thread2.setName("线程2");
thread.start();
thread2.start();
}
}
```
一些注意的细节:
1. `run()`方法和`start()`方法的区别:`start()`方法首先启动线程,然后由JVM调用该线程的`run()`方法。而`run()`方法仅仅封装被线程执行的代码。如果线程没有调用`start()`方法,直接调用它相当于是调用普通方法。
2. Java虚拟机启动时是单线程还是多线程的?答案是:是多线程的,不仅仅是启动main线程,至少还会启动垃圾回收线程。
3. 推荐使用哪种方式创建线程?一般情况下我们推荐使用实现`Runnable`接口的方式。这样可以避免Java单继承带来的局限,增强代码健壮性。
进程切换和线程切换的区别在于,进程切换需要保留当前进程的cpu环境,设置新选中进程的Cpu环境,因而需要花费不少的处理机时间;而线程切换则需要操作系统做的工作要少很多。创建一个新线程的代价要比新进程小得多。在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
Thread类及其常用方法包括常用构造方法、getId()、getName()、getState()、getPriority()、isAlive()、suspend()、resume()、start()、stop()等。其中,常用的构造方法有:
- Thread(Runnable target):使用Runnable对象创建线程对象。
- Thread(String name):创建线程对象并命名。
- Thread(Runnable target, String name):使用 Runnable 对象创建线程对象,并命名。
例如:
```java
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
```
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。是否存活可以通过判断run方法是否运行结束了来确定。
获取当前正在执行线程的引用可以使用currentThread()方法。yield()方法会让别的线程先执行,但不确保真正让出cpu。例如:
```java
Thread.yield(); // 将当前线程由运行态—>就绪态
```
下面是重构后的代码:
public class ThreadYield { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(999999999L); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 等待new Thread所有线程执行完毕,否则一直等待 while(Thread.activeCount() > 1){//使用调试的方式运行 Thread.yield();// 将当前线程由运行态--->就绪态 } System.out.println(Thread.currentThread().getName()); } }
其中,`Thread.yield()`方法用于将当前线程从运行状态变为就绪状态,但不能保证休眠时间是大于等于休眠时间的。如果需要控制线程的休眠时间,可以使用`Thread.sleep()`方法。另外,要中断一个线程,可以使用`interrupt()`方法,但这只是告诉线程需要中断,具体是否能够中断由线程自己决定。
以下是重构后的代码,其中包含了对原始代码的解释:
```java
public class Test4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted()); // 返回中断标志位,并重置标志位
}
}
});
t.start(); // 启动线程
t.interrupt(); // 在循环中中断线程,将中断标志位设置为true
}
}
```
调用`test4()`方法会打印出以下内容:
```
true
false
false
false
false
false
false
false
false
```
解释:在调用`test4()`方法时,首先创建了一个新线程`t`,该线程执行一个循环,循环次数为10次。在每次循环中,通过调用`System.out.println(Thread.interrupted())`打印当前线程的中断标志位,并将其重置为初始状态。在主线程中,先启动了新线程`t`,然后立即调用了`t.interrupt()`中断了该线程。这会导致线程中的中断标志位被设置为`true`,而在循环中的每次迭代都会检查并清除该标志位。因此,在前几次迭代中,输出结果为`true`,表示线程已被中断。随着循环进行,中断标志位被不断清除,所以后续的输出结果为`false`,表示线程未被中断。
public class Test4 { public static void main(String[] args) throws InterruptedException {
test4();
}
public static void test4() throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//System.out.println(Thread.interrupted());//返回中断标志位,并重置标志位
System.out.println(Thread.currentThread().isInterrupted());
}
}
});
t.start(); // t线程中的中断标志位=false
try {
t.join(); // 等待线程t执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt(); // t线程的中断标志位=true
}
}
这段Java代码展示了如何使用线程join()方法。Thread.join()方法使主线程等待,直到指定的线程终止。如果不调用这个方法,那么主线程会立即继续执行,而不会等待新启动的线程结束。
在这段代码中,有三个方法:without(),withoutSleep()和main()。每个方法都创建了一个新的线程并尝试启动它,然后调用了该线程对象的join()方法,以便主线程会等待新启动的线程完成。
1. `without()` 方法先启动新线程t,然后在t线程完成后打印出主线程的名字。
2. 在`withoutSleep()` 方法中,首先让新建的线程执行`try-catch`块中的代码,该代码会使线程暂停5秒(5000毫秒),然后再打印出主线程的名字。接着,我们启动新线程并设置主线程等待新线程的时间上限为2000毫秒(即2秒)。当新的线程在2秒内未完成时,主线程将停止等待并开始执行后续操作。最后,打印出主线程的名字。
3. 在`main()` 方法中,我们首先调用`withoutSleep()` 方法,然后再调用`without()` 方法。这就意味着输出的顺序可能是 "thread-0-> main" 或者 "main-> thread-0"。具体顺序取决于新启动的线程何时完成其任务。
这是你的重构后的代码:
```java
public class ThreadJoin {
public static void without() throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
t.start();
t.join();//等待t线程执行完毕,也就是说先让t执行,主线程等待
System.out.println(Thread.currentThread().getName());
}
public static void withoutSleep() throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
});
t.start();
t.join(2000);//等待线程结束,最多等多少秒
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
withoutSleep();// // 打印顺序:main--->thread-0
}
}
```
Java线程有以下几种状态:新建、就绪、运行、阻塞和死亡。其中,新建状态表示线程对象已经被创建,但还没有启动;就绪状态表示线程已经获得了CPU时间片,可以开始执行;运行状态表示线程正在执行;阻塞状态表示线程因为等待某个对象而无法继续执行;死亡状态表示线程已经结束或者抛出了未捕获的异常 。
public class ThreadState { public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
Thread.interrupted()方法用于判断当前线程的中断标志是否被设置,如果被设置则清除中断标志。理解线程的几种状态有助于更好地掌握线程编程。以下是常见的线程状态:
1. new:指线程实例化后还没有执行start()方法的状态。
2. Runnable状态是指线程进入运行时的状态,它包括运行中和就绪两个状态。比如你今天去银行取钱,正在排队等服务,此时你属于就绪态,而正在让柜台人员办理业务的就是运行中。
3. TERMINATED:线程被销毁时的状态,也就是你去办理业务已经办理完成。
4. TIMED_WAITING:指线程执行了Thread.sleep()方法呈等待状态。等待时间到达后继续向下运行。
5. Blocked:线程阻塞,出现在某一个线程在等待锁的时候。
6. WAITING:指线程执行了Object.wait()等方法后所处的状态。
了解线程安全的概念也很重要。线程安全是指多个线程访问共享资源时,能够保证数据的正确性和一致性。下面是一个关于线程安全的例子:创建了20个线程,每个线程打印10000次,预期结果应该是200000,但实际运行的结果却不是。这便牵扯了我们的线程安全问题。
以下是重构后的代码:
```java
public class UnsafeThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 20; i++) {
count++;
}
}).start();
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}
private static int count = 0;
}
```
我们可以这样认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。线程不安全的原因主要有以下几点:原子性、可见性和代码顺序性。
代码重排序会给多线程带来什么问题?
在单线程情况下,优化是正确的,但在多线程场景下可能会出现问题。例如,快递可能在你写作业的10分钟内被另一个线程放过来,或者被人篡改了。如果指令重排序了代码,就可能导致错误。为了解决线程不安全的问题,Java提供了专业的同步互斥机制。
synchronized关键字—监视锁monitor lock
synchronized的底层是使用操作系统的mutex lock实现的。当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。synchronized用的锁是存在Java对象头里的。synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好。
Java对于多线程的安全问题提供了专业的解决方式:同步互斥机制。同步代码块的语法如下:
```java
synchronized (对象){ // 需要被同步的代码; }
```
synchronized还可以放在方法声明中,表示整个方法为同步方法。例如:
```java
public synchronized void show (String name){ .... }
```
synchronized的锁是什么?
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
- 同步方法的锁:静态方法(类名.class)、非静态方法(this)。
- 同步代码块:自己指定,很多时候也是指定为this或类名.class。注意:必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全。一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)。
```java
public class SynchronizedDemo {
public synchronized void method() {
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method(); // 进入方法会锁 demo 指向对象中的锁;出方法会释放 demo 指向的对象中的锁
}
}
```
锁的 SynchronizedDemo 类的对象:
```java
public class SynchronizedDemo {
public synchronized static void method() {
}
public static void main(String[] args) {
method(); // 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放 SynchronizedDemo.class 指向的对象中的锁
}
}
```
什么时候释放锁?:
1. 当前线程的同步方法、同步代码块执行结束。
2. 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。注意线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁。
volatile 关键字:
是一种轻量级的同步机制。修饰的共享变量,可以保证(变量读取时的)可见性,部分保证顺序性,不能保证原子性。提示线程每次从共享内存中读取变量而不是从私有内存中读取。
```java
class ThraedDemo {
private volatile int n;
}
```
```java
public class ThreadCommunication implements Runnable {
int i = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ": " + i++);
} else {
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ThreadTest1 {
public static void main(String[] args) {
ThreadCommunication t = new ThreadCommunication();
Thread thread = new Thread(t);
Thread thread2 = new Thread(t);
thread.setName("线程1");
thread2.setName("线程2");
thread.start();
thread2.start();
}
}
```
wait():使当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
notifyAll ():唤醒正在排队等待资源的所有线程结束等待。这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
单例模式:
立即加载-饿汉式:使用类的时候已经将对象创建完毕,常见的办法就是直接new实例化。
```java
class Singleton1 {//饿汉式,getInstance方法没有同步,可能出现非线程安全问题
private static Singleton1 instance=new Singleton1();
private Singleton1() { }
public static Singleton1 getInstance() {
return instance;
}
}
```
延迟加载-懒汉模式:在调用方法实例时才被创建。
```java
class Singleton2 {//懒汉式单线程版
private static Singleton2 instance=null;
private Singleton2() {}
public static synchronized Singleton2 getInstance() {
if (instance==null) {
instance=new Singleton2();
}
return instance;
}
}
```
Singleton3 类的懒汉式多线程版实现如下:
```java
class Singleton3 {//懒汉式多线程版,效率低下一个线程要想取得对象必须等上一个线程释放锁才可以继续执行。
private static Singleton3 instance = null;
private Singleton3() {}
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
```
双重校验锁(DCL)的实现如下:
```java
class Singleton3 {//懒汉式多线程版,效率低下一个线程要想取得对象必须等上一个线程释放锁才可以继续执行。
private static volatile Singleton3 instance = null; //使用volatile关键字确保实例变量在多线程环境下的可见性
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) { //同步代码块
if (instance == null) { //第一重检查实例是否存在
instance = new Singleton3(); //第二重检查实例是否存在,如果不存在则创建实例
}
}
}
return instance;
}
}
```
进程和线程是操作系统中的两个概念。进程是一个程序在运行时的一个实例,而线程是CPU调度的基本单位,同一进程中的线程切换,不会引起进程切换。多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间) 。
至于面试题中提到的问题,线程死了进程会死吗?答案是不会。因为线程是进程的一部分,所以当一个线程死亡时,它只是被销毁了而已,而不会影响到整个进程 。
进程和线程是操作系统进行资源调度和分配的基本单位。进程间独享内存,一个系统至少有一个进程;线程间共享进程的内存,一个进程至少有一个线程。当进程或线程出现异常或错误时,可能会导致进程或线程死亡。
volatile和synchronized是Java中用于实现线程同步的两种机制。它们的区别如下:
1. volatile不会进行加锁操作:volatile变量是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,因此也不会使执行线程阻塞。这使得volatile变量比synchronized关键字更轻量级。
2. volatile变量作用类似于同步变量读写操作:从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
3. volatile不如synchronized安全:在代码中过度依赖volatile变量来控制状态的可见性时,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,使用同步机制会更安全些。
4. volatile无法同时保证内存可见性和原子性:加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。如果声明为volatile的简单变量与该变量以前的值相关,那么volatile关键字不起作用,这样的表达式都不是原子操作,如"count++"、"count = count + 1"等。
sleep和wait分别是Thread类和Object类的方法,它们的主要区别如下:
- sleep和wait方法所属的类不同:sleep是Thread类的方法,而wait是Object类的方法。
- 调用方式不同:sleep()方法是Thread类的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程。而wait()方法是Object类的方法,调用对象的wait()方法会导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool)。
- 唤醒方式不同:只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入就绪状态。如果线程重新获得对象的锁就可以进入就绪状态。