java多线程详解_java什么是多线程

    System.out.println("main end...");
}

}


`sleep()`传入的参数是毫秒。调整暂停时间的大小,我们可以看到`main`线程和`t`线程执行的先后顺序。


要特别注意:直接调用`Thread`实例的`run()`方法是无效的:



public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.run();
}
}

class MyThread extends Thread {
public void run() {
System.out.println(“hello”);
}
}


直接调用`run()`方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码实际上是在`main()`方法内部又调用了`run()`方法,打印`hello`语句是在`main`线程中执行的,没有任何新线程被创建。


必须调用`Thread`实例的`start()`方法才能启动新线程,如果我们查看`Thread`类的源代码,会看到`start()`方法内部调用了一个`private native void start0()`方法,`native`修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。


#### 线程的优先级


可以对线程设定优先级,设定优先级的方法是:



Thread.setPriority(int n) // 1~10, 默认值5


优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。


#### 练习


从[[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LiunirUz-1640516280422)(data:image/png;base64,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)]]( )下载练习:[创建新线程]( ) (推荐使用[IDE练习插件]( )快速下载)


#### 小结


Java用`Thread`对象表示一个线程,通过调用`start()`启动一个新线程;


一个线程对象只能调用一次`start()`方法;


线程的执行代码写在`run()`方法中;


线程调度由操作系统决定,程序本身无法决定调度顺序;


`Thread.sleep()`可以把当前线程暂停一段时间。




---


### 线程的状态


在Java程序中,一个线程对象只能调用一次`start()`方法启动新线程,并在新线程中执行`run()`方法。一旦`run()`方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:


* New:新创建的线程,尚未执行;
* Runnable:运行中的线程,正在执行`run()`方法的Java代码;
* Blocked:运行中的线程,因为某些操作被阻塞而挂起;
* Waiting:运行中的线程,因为某些操作在等待中;
* Timed Waiting:运行中的线程,因为执行`sleep()`方法正在计时等待;
* Terminated:线程已终止,因为`run()`方法执行完毕。


用一个状态转移图表示如下:



     ┌─────────────┐
     │     New     │
     └─────────────┘
            │
            ▼

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌─────────────┐ ┌─────────────┐
││ Runnable │ │ Blocked ││
└─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
│ Waiting │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─


┌─────────────┐
│ Terminated │
└─────────────┘


当线程启动后,它可以在`Runnable`、`Blocked`、`Waiting`和`Timed Waiting`这几个状态之间切换,直到最后变成`Terminated`状态,线程终止。


线程终止的原因有:


* 线程正常终止:`run()`方法执行到`return`语句返回;
* 线程意外终止:`run()`方法因为未捕获的异常导致线程终止;
* 对某个线程的`Thread`实例调用`stop()`方法强制终止(强烈不推荐使用)。


一个线程还可以等待另一个线程直到其运行结束。例如,`main`线程在启动`t`线程后,可以通过`t.join()`等待`t`线程结束后再继续运行:


`// 多线程` Run



public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println(“hello”);
});
System.out.println(“start”);
t.start();
t.join();
System.out.println(“end”);
}
}


当`main`线程对线程对象`t`调用`join()`方法时,主线程将等待变量`t`表示的线程运行结束,即`join`就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是`main`线程先打印`start`,`t`线程再打印`hello`,`main`线程最后再打印`end`。


如果`t`线程已经结束,对实例`t`调用`join()`会立刻返回。此外,`join(long)`的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。


#### 小结


Java线程对象`Thread`的状态包括:`New`、`Runnable`、`Blocked`、`Waiting`、`Timed Waiting`和`Terminated`;


通过对另一个线程对象调用`join()`方法可以等待其执行结束;


可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;


对已经运行结束的线程调用`join()`方法会立刻返回。




---


### 中断线程


如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行`run()`方法,使得自身线程能立刻结束运行。


我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。


中断一个线程非常简单,只需要在其他线程中对目标线程调用`interrupt()`方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。


我们还是看示例代码:


`// 中断线程` Run



public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println(“end”);
}
}

class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}


仔细看上述代码,`main`线程通过调用`t.interrupt()`方法中断`t`线程,但是要注意,`interrupt()`方法仅仅向`t`线程发出了“中断请求”,至于`t`线程是否能立刻响应,要看具体代码。而`t`线程的`while`循环会检测`isInterrupted()`,所以上述代码能正确响应`interrupt()`请求,使得自身立刻结束运行`run()`方法。


如果线程处于等待状态,例如,`t.join()`会让`main`线程进入等待状态,此时,如果对`main`线程调用`interrupt()`,`join()`方法会立刻抛出`InterruptedException`,因此,目标线程只要捕获到`join()`方法抛出的`InterruptedException`,就说明有其他线程对其调用了`interrupt()`方法,通常情况下该线程应该立刻结束运行。


我们来看下面的示例代码:


`// 中断线程` Run



public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println(“end”);
}
}

class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println(“interrupted!”);
}
hello.interrupt();
}
}

class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}


`main`线程通过调用`t.interrupt()`从而通知`t`线程中断,而此时`t`线程正位于`hello.join()`的等待中,此方法会立刻结束等待并抛出`InterruptedException`。由于我们在`t`线程中捕获了`InterruptedException`,因此,就可以准备结束该线程。在`t`线程结束前,对`hello`线程也进行了`interrupt()`调用通知其中断。如果去掉这一行代码,可以发现`hello`线程仍然会继续运行,且JVM不会退出。


另一个常用的中断线程的方法是设置标志位。我们通常会用一个`running`标志位来标识线程是否应该继续运行,在外部线程中,通过把`HelloThread.running`置为`false`,就可以让线程结束:


`// 中断线程` Run



public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}

class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println(“end!”);
}
}


注意到`HelloThread`的标志位`boolean running`是一个线程间共享的变量。线程间共享变量需要使用`volatile`关键字标记,确保每个线程都能读取到更新后的变量值。


为什么要对线程间共享的变量用关键字`volatile`声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!



┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Main Memory
│ │
┌───────┐┌───────┐┌───────┐
│ │ var A ││ var B ││ var C │ │
└───────┘└───────┘└───────┘
│ │ ▲ │ ▲ │
─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
│ │ │ │
┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐
▼ │ ▼ │
│ ┌───────┐ │ │ ┌───────┐ │
│ var A │ │ var C │
│ └───────┘ │ │ └───────┘ │
Thread 1 Thread 2
└ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘


这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量`a = true`,线程1执行`a = false`时,它在此刻仅仅是把变量`a`的副本变成了`false`,主内存的变量`a`还是`true`,在JVM把修改后的`a`回写到主内存之前,其他线程读取到的`a`的值仍然是`true`,这就造成了多线程之间共享的变量不一致。


因此,`volatile`关键字的目的是告诉虚拟机:


* 每次访问变量时,总是获取主内存的最新值;
* 每次修改变量后,立刻回写到主内存。


`volatile`关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。


如果我们去掉`volatile`关键字,运行上述程序,发现效果和带`volatile`差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。


#### 小结


对目标线程调用`interrupt()`方法可以请求中断一个线程,目标线程通过检测`isInterrupted()`标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到`InterruptedException`;


目标线程检测到`isInterrupted()`为`true`或者捕获了`InterruptedException`都应该立刻结束自身线程;


通过标志位判断需要正确使用`volatile`关键字;


`volatile`关键字解决了共享变量在线程间的可见性问题。




---


### 守护线程


Java程序入口就是由JVM启动`main`线程,`main`线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。


如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。


但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:



class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}


如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?


然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?


答案是使用守护线程(Daemon Thread)。


守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。


因此,JVM退出时,不必关心守护线程是否已结束。


如何创建守护线程呢?方法和普通线程一样,只是在调用`start()`方法前,调用`setDaemon(true)`把该线程标记为守护线程:



Thread t = new MyThread();
t.setDaemon(true);
t.start();


在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。


#### 练习


从[[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b30HBfgd-1640516280422)(data:image/png;base64,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)]]( )下载练习:[使用守护线程]( ) (推荐使用[IDE练习插件]( )快速下载)


#### 小结


守护线程是为其他线程服务的线程;


所有非守护线程都执行完毕后,虚拟机退出;


守护线程不能持有需要关闭的资源(如打开文件等)。




---


### 线程同步


当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。


这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。


我们来看一个例子:


`// 多线程` Run



public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}

class Counter {
public static int count = 0;
}

class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count += 1; }
}
}

class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count -= 1; }
}
}


上面的代码很简单,两个线程同时对一个`int`变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。


这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。


例如,对于语句:



n = n + 1;


看上去是一行语句,实际上对应了3条指令:



ILOAD
IADD
ISTORE


我们假设`n`的值是`100`,如果两个线程同时执行`n = n + 1`,得到的结果很可能不是`102`,而是`101`,原因在于:



┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IADD
│ │ISTORE (101)
│IADD │
│ISTORE (101)│
▼ ▼


如果线程1在执行`ILOAD`后被操作系统中断,此刻如果线程2被调度执行,它执行`ILOAD`后获取的值仍然是`100`,最终结果被两个线程的`ISTORE`写入后变成了`101`,而不是期待的`102`。


这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:



┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│-- lock – │
│ILOAD (100) │
│IADD │
│ISTORE (101) │
│-- unlock – │
│ │-- lock –
│ │ILOAD (101)
│ │IADD
│ │ISTORE (102)
│ │-- unlock –
▼ ▼


通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。


可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用`synchronized`关键字对一个对象进行加锁:



synchronized(lock) {
n = n + 1;
}


`synchronized`保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用`synchronized`改写如下:


`// 多线程` Run



public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}

class Counter {
public static final Object lock = new Object();
public static int count = 0;
}

class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.count += 1;
}
}
}
}

class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.count -= 1;
}
}
}
}


注意到代码:



synchronized(Counter.lock) { // 获取锁

} // 释放锁


它表示用`Counter.lock`实例作为锁,两个线程在执行各自的`synchronized(Counter.lock) { ... }`代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在`synchronized`语句块结束会自动释放锁。这样一来,对`Counter.count`变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。


使用`synchronized`解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为`synchronized`代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,`synchronized`会降低程序的执行效率。


我们来概括一下如何使用`synchronized`:


1. 找出修改共享变量的线程代码块;
2. 选择一个共享实例作为锁;
3. 使用`synchronized(lockObject) { ... }`。


在使用`synchronized`的时候,不必担心抛出异常。因为无论是否有异常,都会在`synchronized`结束处正确释放锁:



public void add(int m) {
synchronized (obj) {
if (m < 0) {
throw new RuntimeException();
}
this.value += m;
} // 无论有无异常,都会在此释放锁
}


我们再来看一个错误使用`synchronized`的例子:


`// 多线程` Run



public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}

class Counter {
public static final Object lock1 = new Object();
public static final Object lock2 = new Object();
public static int count = 0;
}

class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock1) {
Counter.count += 1;
}
}
}
}

class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock2) {
Counter.count -= 1;
}
}
}
}


结果并不是0,这是因为两个线程各自的`synchronized`锁住的*不是同一个对象*!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。


因此,使用`synchronized`的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。


我们再看一个例子:


`// 多线程` Run



public class Main {
public static void main(String[] args) throws Exception {
var ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };
for (var t : ts) {
t.start();
}
for (var t : ts) {
t.join();
}
System.out.println(Counter.studentCount);
System.out.println(Counter.teacherCount);
}
}

class Counter {
public static final Object lock = new Object();
public static int studentCount = 0;
public static int teacherCount = 0;
}

class AddStudentThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.studentCount += 1;
}
}
}
}

class DecStudentThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.studentCount -= 1;
}
}
}
}

class AddTeacherThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.teacherCount += 1;
}
}
}
}

class DecTeacherThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.teacherCount -= 1;
}
}
}
}


上述代码的4个线程对两个共享变量分别进行读写操作,但是使用的锁都是`Counter.lock`这一个对象,这就造成了原本可以并发执行的`Counter.studentCount += 1`和`Counter.teacherCount += 1`,现在无法并发执行了,执行效率大大降低。实际上,需要同步的线程可以分成两组:`AddStudentThread`和`DecStudentThread`,`AddTeacherThread`和`DecTeacherThread`,组之间不存在竞争,因此,应该使用两个不同的锁,即:


`AddStudentThread`和`DecStudentThread`使用`lockStudent`锁:



synchronized(Counter.lockStudent) {

}


`AddTeacherThread`和`DecTeacherThread`使用`lockTeacher`锁:



synchronized(Counter.lockTeacher) {

}


这样才能最大化地提高执行效率。


#### 不需要synchronized的操作


JVM规范定义了几种原子操作:


* 基本类型(`long`和`double`除外)赋值,例如:`int n = m`;
* 引用类型赋值,例如:`List<String> list = anotherList`。


`long`和`double`是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把`long`和`double`的赋值作为原子操作实现的。


单条原子操作的语句不需要同步。例如:



public void set(int m) {
synchronized(lock) {
this.value = m;
}
}


就不需要同步。


对引用也是类似。例如:



public void set(String s) {
this.value = s;
}


上述赋值语句并不需要同步。


但是,如果是多行赋值语句,就必须保证是同步操作,例如:



class Pair {
int first;
int last;
public void set(int first, int last) {
synchronized(this) {
this.first = first;
this.last = last;
}
}
}


有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:



class Pair {
int[] pair;
public void set(int first, int last) {
int[] ps = new int[] { first, last };
this.pair = ps;
}
}


就不再需要同步,因为`this.pair = ps`是引用赋值的原子操作。而语句:



int[] ps = new int[] { first, last };


这里的`ps`是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。


#### 小结


多线程同时读写共享变量时,会造成逻辑错误,因此需要通过`synchronized`同步;


同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;


注意加锁对象必须是同一个实例;


对JVM定义的单个原子操作不需要同步。




---


### 同步方法


我们知道Java程序依靠`synchronized`对线程进行同步,使用`synchronized`的时候,锁住的是哪个对象非常重要。


让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把`synchronized`逻辑封装起来。例如,我们编写一个计数器如下:



public class Counter {
private int count = 0;

public void add(int n) {
    synchronized(this) {
        count += n;
    }
}

public void dec(int n) {
    synchronized(this) {
        count -= n;
    }
}

public int get() {
    return count;
}

}


这样一来,线程调用`add()`、`dec()`方法时,它不必关心同步逻辑,因为`synchronized`代码块在`add()`、`dec()`方法内部。并且,我们注意到,`synchronized`锁住的对象是`this`,即当前实例,这又使得创建多个`Counter`实例的时候,它们之间互不影响,可以并发执行:



var c1 = Counter();
var c2 = Counter();

// 对c1进行操作的线程:
new Thread(() -> {
c1.add();
}).start();
new Thread(() -> {
c1.dec();
}).start();

// 对c2进行操作的线程:
new Thread(() -> {
c2.add();

难道这样就够了吗?不,远远不够!

提前多熟悉阿里往年的面试题肯定是对面试有很大的帮助的,但是作为技术性职业,手里有实打实的技术才是你面对面试官最有用的利器,这是从内在散发出来的自信。

备战阿里时我花的最多的时间就是在学习技术上,占了我所有学习计划中的百分之70,这是一些我学习期间觉得还是很不错的一些学习笔记

我为什么要写这篇文章呢,其实我觉得学习是不能停下脚步的,在网络上和大家一起分享,一起讨论,不单单可以遇到更多一样的人,还可以扩大自己的眼界,学习到更多的技术,我还会在csdn、博客、掘金等网站上分享技术,这也是一种学习的方法。

今天就分享到这里了,谢谢大家的关注,以后会分享更多的干货给大家!

阿里一面就落马,恶补完这份“阿里面试宝典”后,上岸蚂蚁金服

阿里一面就落马,恶补完这份“阿里面试宝典”后,上岸蚂蚁金服

image.png

public void dec(int n) {
synchronized(this) {
count -= n;
}
}

public int get() {
    return count;
}

}


这样一来,线程调用`add()`、`dec()`方法时,它不必关心同步逻辑,因为`synchronized`代码块在`add()`、`dec()`方法内部。并且,我们注意到,`synchronized`锁住的对象是`this`,即当前实例,这又使得创建多个`Counter`实例的时候,它们之间互不影响,可以并发执行:



var c1 = Counter();
var c2 = Counter();

// 对c1进行操作的线程:
new Thread(() -> {
c1.add();
}).start();
new Thread(() -> {
c1.dec();
}).start();

// 对c2进行操作的线程:
new Thread(() -> {
c2.add();

难道这样就够了吗?不,远远不够!

提前多熟悉阿里往年的面试题肯定是对面试有很大的帮助的,但是作为技术性职业,手里有实打实的技术才是你面对面试官最有用的利器,这是从内在散发出来的自信。

备战阿里时我花的最多的时间就是在学习技术上,占了我所有学习计划中的百分之70,这是一些我学习期间觉得还是很不错的一些学习笔记

我为什么要写这篇文章呢,其实我觉得学习是不能停下脚步的,在网络上和大家一起分享,一起讨论,不单单可以遇到更多一样的人,还可以扩大自己的眼界,学习到更多的技术,我还会在csdn、博客、掘金等网站上分享技术,这也是一种学习的方法。

今天就分享到这里了,谢谢大家的关注,以后会分享更多的干货给大家!

[外链图片转存中…(img-rs5hhlrg-1719268706431)]

[外链图片转存中…(img-hU1p0xp6-1719268706431)]

[外链图片转存中…(img-e2iDBrRH-1719268706432)]

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值