线程与进程
进程(Process)
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早起面向进程的计算机结构中, 进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据以及组织形式的描述,进程是程序的实体。
线程(Thread)
线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
联系与区别
简单来说:
进程是一个容器。比如一个家庭(一家三口)的生活,家里有电视、冰箱、洗衣机还有两个卧室。这个大环境来说就可以说是个进程,而三个人就是3个线程。他们可能互相不冲突,比如妈妈在卧室休息,爸爸在看电视,女儿在洗衣服。又可能有冲突,比如女儿在看动画片的时候爸爸就不能看体育频道了。
线程就是轻量级的进程,是程序执行的最小单位,一个进程至少含有一个线程.
使用多线程是因为线程间的切换和调度的成本远远小于进程
线程的基本操作
新建线程
方法1:继承Thread的run方法创建线程:
通过继承Thread类来创建并启动多线程的一般步骤如下
(1)d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
(2)创建Thread子类的实例,也就是创建了线程对象
(3)启动线程,即调用线程的start()方法
注意:不要用run()来开启新线程,他只会在当前线程中串行执行run()中的代码,即为运行run()方法
class MyThread1 extends Thread {
public MyThread1(String name) {
super(name);
}
@Override
public synchronized void run() {
System.out.println("继承thread使用run()方法启动线程:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) throws Exception {
//方法1:
MyThread1 thread1 = new MyThread1("A");
thread1.start();
}
方法2:实现Runnable的run方法创建线程(常用)
实现Runnable接口创建线程的步骤为:
(1)创建一个类并实现Runnable接口
(2)重写run()方法,将所要完成的任务代码写进run()方法中
(3)创建实现Runnable接口的类的对象,将该对象当做Thread类的构造方法中的参数传进去
(4)使用Thread类的构造方法创建一个对象,并调用start()方法即可运行该线程
class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable的run()方法启动线程:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) throws Exception {
//方法2
Thread thread2 = new Thread(new MyThread2());
thread2.start();
}
方法3:使用Callable和Future创建线程
实现Callable接口创建线程的步骤为:
(1)创建一个类并实现Callable接口
(2)重写call()方法,将所要完成的任务的代码写进call()方法中,需要注意的是call()方法有返回值,并且可以抛出异常
(3)如果想要获取运行该线程后的返回值,需要创建Future接口的实现类的对象,即FutureTask类的对象,调用该对象的get()方法可获取call()方法的返回值
(4)使用Thread类的有参构造器创建对象,将FutureTask类的对象当做参数传进去,然后调用start()方法开启并运行该线程。
class MyThread3 implements Callable{
@Override
public Object call() throws Exception {
System.out.println("实现callable的call()方法启动线程:"+ Thread.currentThread().getName());
return null;
}
}
public static void main(String[] args) throws Exception {
//方法3
MyThread3 thread3 = new MyThread3();
FutureTask futureTask = new FutureTask(thread3);
new Thread(futureTask).start();
}
方法4:使用线程池例如用Executor框架
使用线程池创建线程的步骤:
(1)使用Executors类中的newFixedThreadPool(int num)方法创建一个线程数量为num的线程池
(2)调用线程池中的execute()方法执行由实现Runnable接口创建的线程;调用submit()方法执行由实现Callable接口创建的线程
(3)调用线程池中的shutdown()方法关闭线程池
class MyThread4 implements Runnable{
private int num;
public MyThread4(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println("正在执行task " + num );
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task " + num + "执行完毕");
}
}
public static void main(String[] args) throws Exception {
//方法4
//设置核心池大小
int corePoolSize = 5;
//设置线程池最大能接受多少线程
int maximumPoolSize = 10;
//当前线程数大于corePoolSize、小于maximumPoolSize时,超出corePoolSize的线程数的生命周期
long keepActiveTime = 200;
//设置时间单位,秒
TimeUnit timeUnit = TimeUnit.SECONDS;
//设置线程池缓存队列的排队策略为FIFO,并且指定缓存队列大小为5
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(5);
//创建ThreadPoolExecutor线程池对象,并初始化该对象的各种参数
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepActiveTime, timeUnit,workQueue);
//往线程池中循环提交线程
for (int i = 0; i < 3; i++) {
//创建线程类对象
MyThread4 thread4 = new MyThread4(i);
//开启线程
executor.execute(thread4);
//获取线程池中线程的相应参数
System.out.println("线程池中线程数目:" +executor.getPoolSize() + ",队列中等待执行的任务数目:"+executor.getQueue().size() + ",已执行完的任务数目:"+executor.getCompletedTaskCount());
}
//待线程池以及缓存队列中所有的线程任务完成后关闭线程池。
executor.shutdown();
}
终止线程(stop )
一般来说,线程在执行完毕后就会自动结束,无需手动关闭。但是,凡是都有例外。一些服务端后台线程可能会常驻系统,他们通常不会正常中介。比如,他们的执行体本身就是一个大大的无穷循环,用于提供某些服务。
查阅JDK不嫩发现Thread提供了一个stop()方法。如果你使用stop()方法,就可以立即将一个线程停止,非常方便。但是该方法已经被弃用
了。
弃用原因
sto方法太过于暴力了,强行把一些正在执行的线程终止,他可能会造成一些数据不一致的问题。
下面我几个例子说明,假如数据库中维护着一张表,里面记录了用户ID和用户名。假设有两条记录:
记录1:id=1,name=张三;
记录2:id=2,name=李四;
如果我们使用一个User对象去保护这些记录,我们肯定会希望这个User对象完整的存储,如果User对象存储各存一半,我相信大部分人都会头疼。
为什么会出现这种情况呢?
Thread.stop()方法在结束线程时,会直接终止线程,并like释放这个线程锁持有的锁。而这些锁恰恰就是维护对象一致性的。如果此时,写线程写入数据写了一般,并强行终止,那么对象就会被写坏,同事由于锁已经被释放,另一个等待该锁的度线程就读到了不一样的对象。
请看实例代码:
public class StopError {
public static User user = new User();
public static class ChangThread extends Thread {
@Override
public void run() {
while (true) {
synchronized (user) {
int v = (int) (System.currentTimeMillis() / 1000);
user.setId(v);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setName(String.valueOf(v));
}
}
}
}
public static class ReadThread extends Thread{
@Override
public void run() {
while (true) {
synchronized (user) {
if(user.getId()!=Integer.parseInt(user.getName())){
System.out.println(user.toString());
}
}
yield();
}
}
}
public static void main(String[] args) throws InterruptedException {
new ReadThread().start();
while (true){
Thread thread = new ChangThread();
thread.start();
Thread.sleep(150);
thread.stop();
}
}
}
class User {
int id;
public User() {
this.id = -1;
this.name = "-1";
}
String name;
public int getId() {
return id;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
运行结果:
线程中断
在java中线程中断是一个重要的线程协作机制。从字面上理解,中断就是让目标线程停止执行的意思,而实际上并非完全如此。在上面我们已经讨论过stop方法停止线程的害处。强大的JDK又给我们提供了更方便的方法,那就是线程中断。
严格的来说,线程中断并不会立即退出线程,而是给线程发一个通知,告诉目标线程你应该退出了,至于目标线程后续如何工作,那就是目标现成自行决定了。不立即退出也是中断和停止线程的区别
有关中断的有三个方法,他们看起来像是三胞胎,可能会引起混淆,请大家注意
方法 | 含义 |
---|---|
public void Thread.interrupt() | 中断线程 |
public boolean Thread.isInterrupted() | 判断线程是否被中断 |
public static boolean Thread.interrupted() | 判断线程是否被中断并清除当前中断状态 |
下面请看代码并分析,线程会立刻被停止吗?
public static void main(String[] args) throws InterruptedException {
int i = 0;
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
System.out.println(i);
Thread.yield();
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
这里虽然中断了,但是线程并没有对中断处理的逻辑,所以即使线程被中断也不会发生任何作用。如果希望中断后退出,就必须为他加上中断处理代码:
public static void main(String[] args) throws InterruptedException {
int i = 0;
Thread t2 = new Thread() {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("线程已经中断了");
break;
}
System.out.println(i+1);
Thread.yield();
}
}
};
t2.start();
Thread.sleep(2000);
t2.interrupt();
}
t1线程是一个死循环,一直输出0,
t2线程是一个可以结束的线程,当输出线程被中断后就结束执行
线程休息(sleep)
public static native void sleep(long millis) throws InterruptedException
Thread.sleep方法会让当前线程休息若干时间,它会抛出一个InterruptedException异常。不是运行异常,也就是说程序必须捕获并且处理它,当线程在sleep休眠时,如果被中断,这个异常就会产生。
package com.jingchu.thread.sleep;
/**
* @description: 休息是中断产生异常实例
* @author: JingChu
* @createtime :2020-07-20 12:30:41
**/
public class MySleep {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("线程被中断了");
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
System.out.println("设置中断状态");
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
};
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
}
运行结果:
注意:Thread.sleep()方法由于中断而抛出异常,此时,他会清除中断记忆,如果不加处理,下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标志位。
等待(wait)和通知(notify)
为了支持多线程间的协作,JDK提供了两个非常重要的接口,wait和notify,这两个不是在Thread中而是在Object类,也就意味着任何对象都可以调用这两个方法
public final void wait() throws InterruptedExption
public final native void notify()
当在一个对象实例上调用wait方法后,当前线程就会在这个对象上等待。比如,线程A调用了obj.wait(),那么线程A就会停止继续执行,而转为等待状态。等到到合适结束呢?线程A则会一直等待到其他线程调用了obj.notify()。这时,obj就成了多个线程间有效的通信手段。
当notify被调用时,会从等待队列中随机选择一个线程并将它唤醒,并不是先等待的线程就先有优先权,这个选择完全是随机的。
除了notify方法外,还有一个类似的方法notifyAll,不同的是,这个方法会唤醒在这等待队列中所有等待的线程,而不是随机一个
还需要强调一点就是,Object.wait()并不是可以随便调用的,必须包含在对应的synchronzied语句中,无论是wait()还是notify()都需要首先获得目标对象的监视器。
下面请看案例:
package com.jingchu.thread.communication;
/**
* @description: 线程通信实例
* @author: JingChu
* @createtime :2020-07-20 12:46:09
**/
public class MyWait {
final static Object object = new Object();
public static class Thread1 extends Thread{
@Override
public void run() {
synchronized (object){
System.out.println("线程1启动:");
try{
System.out.println("线程1进入等待");
object.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("线程1结束");
}
}
}
public static class Thread2 extends Thread{
@Override
public void run() {
synchronized (object){
System.out.println("线程2通知线程1可以运行");
object.notify();
System.out.println("线程2结束");
try{
Thread.sleep(200);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread thread1 = new Thread1();
Thread thread2 = new Thread2();
thread1.start();
thread2.start();
}
}
运行结果
解析:
上述代码中,开启了两个线程,执行了obj.wait()方法。注意:程序在执行wait()方法钱先申请了对象锁。
所以wait他是持有obj的锁,wait方法执行后,thread1进行等待,并释放obj的所。thread2在执行notify后也会先获得obj的锁。
注意:Thread.sleep()和wait()都可以让线程等待若干时间。除了wait()可以被唤醒外,另一个主要区别就是wait()方法会释放目标对象的锁。
挂起(suspend )和继续执行(resume )
挂起和继续执行是一对相反的操作,被挂起的线程必须要等到resume操作后,才可以继续制定。乍看一下感觉这个很好用,但是其实他们已经被停用了。
弃用原因
因为suspend在导致线程暂停的同时,并不会释放任何锁资源,这就会导致其他任何线程想访问被他暂听使用的锁时,都会被牵连,导致无法正常运行。直到进行了resume()操作,被挂起的线程才能继续,从而其他阻塞在相关锁上的线程也可以继续执行。但是如果resume()操作意外在suspend()前执行了,那么被挂起的线程就很有可能继续执行。并且,更严重的是:它占用的锁不会被释放,这可能导致整个系统工作不正常。
代码示例:
package com.jingchu.thread.suspend;
/**
* @description: 挂起和继续执行测试
* @author: JingChu
* @createtime :2020-07-20 13:04:30
**/
public class Mysuspend {
public static Object object = new Object();
static ChangeThread t1 = new ChangeThread("t1");
static ChangeThread t2 = new ChangeThread("t2");
public static class ChangeThread extends Thread {
public ChangeThread(String name) {
super(name);
}
@Override
public void run() {
synchronized (object) {
System.out.println("线程正在执行:" + Thread.currentThread().getName());
Thread.currentThread().suspend();
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000);
t2.start();
t1.resume();
t2.resume();
t1.join();
t2.join();
}
}
虽然我们看到了程序的输出,但是程序并没有结束,这说明线程被挂起了。
等待线程结束(join)和谦让(yield)
很多情况下,一个线程的输入依赖于另外线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来支持这个功能,如下所示,展示了2个join()方法。
public final void join() throws InterruptException
public final syn chronized void join(long millis) throwsInterruptedException
第一个方法表示无限等待,他会一直阻塞当前线程,知道线程执行完毕。第二个方法给出了最大等待时间,如果超出等待时间,会继续往下执行。
下面请看实例代码:
package com.jingchu.thread.join;
/**
* @description: 线程等待实例
* @author: JingChu
* @createtime :2020-07-20 13:14:25
**/
public class MyJoin {
public volatile static int i=0;
public static class AddThread extends Thread{
@Override
public void run() {
for(i=0;i<100000;i++) {
}
}
public static void main(String[] args) throws InterruptedException {
AddThread at = new AddThread();
at.start();
at.join();
System.out.println(i);
}
}
}
结果:
主函数中如果不使用join等待AddThread,那么输出的值就是0,因为还没等待AddThread执行,就直接输出了。如果使用了join则表示Main线程愿意等待addThread线程执行结束,所以i总是100000
public static native void yield()
这是一个静态方法,一但执行,他会让出CPU,但是需要注意,让出CPU不代表当前线程就不执行了。当前线程会加入到CPU资源的争夺,但是能否被再分配就不一定了。
线程组
在一个系统中,如果线程的数量很多,而且分工能却,我们就可以把相同功能的相乘分成一组。
下面请看例子:
package com.jingchu.thread.group;
/**
* @description: 线程组测试实例
* @author: JingChu
* @createtime :2020-07-20 13:26:18
**/
public class MyThreadGroup implements Runnable {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getThreadGroup().getName()+"-"+Thread.currentThread().getName());
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("线程组");
Thread t1 = new Thread(threadGroup,new MyThreadGroup(),"T1");
Thread t2 = new Thread(threadGroup,new MyThreadGroup(),"T2");
t1.start();
t2.start();
System.out.println(threadGroup.activeCount());
threadGroup.list();
}
}
运行结果
守护线程(Deamon)
守护线程是一个特殊的线程,是系统的守护者,在后台默默的完成系统的服务,比如垃圾回收。用户线程可以当做是系统的工作线程,它会完成系统要完成的业务操作,如果所有的用户线程都结束了,那么守护线程自然也就结束了。
package com.jingchu.thread.deamon;
/**
* @description: 守护线程
* @author: JingChu
* @createtime :2020-07-20 13:31:16
**/
public class MyDeamon {
public static class Deamon extends Thread{
@Override
public void run() {
System.out.println("我是一个活跃的线程");
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t = new Deamon();
t.setDaemon(true);
t.start();
Thread.sleep(2000);
}
}
注意:设置守护线程必须在线程start()之前,否则你会获得一个异常,告诉你设置守护线程失败
线程的优先级
在Java中线程有自己的优先级,优先级越高的线程在竞争资源师更有优势,但不是说具有绝对性的优势。线程的优先级范围是1-10,数字越大优先级越高。
下面请看例子
package com.jingchu.thread.priority;
/**
* @description: 线程的优先级测试
* @author: JingChu
* @createtime :2020-07-20 13:38:24
**/
public class MyPriority {
public static class HPriority extends Thread{
static int count=0;
@Override
public void run() {
while (true){
synchronized (MyPriority.class){
count++;
if(count>1000000){
System.out.println("高优先级线程结束");
break;
}
}
}
}
}
public static class Lpriority extends Thread{
static int count=0;
@Override
public void run() {
while (true){
synchronized (MyPriority.class){
count++;
if(count>1000000){
System.out.println("低优先级线程结束");
break;
}
}
}
}
}
public static class Mpriority extends Thread{
static int count=0;
@Override
public void run() {
while (true){
synchronized (MyPriority.class){
count++;
if(count>1000000){
System.out.println("中优先级线程结束");
break;
}
}
}
}
}
public static void main(String[] args) {
Thread high = new HPriority();
Thread low = new Lpriority();
Thread mium = new Mpriority();
high.setPriority(9);
low.setPriority(3);
mium.setPriority(6);
high.start();
low.start();
mium.start();
}
}
绝大部分运行结果都是高中低,但是偶尔有特殊情况。
synchronized
线程安全:
一般来说,程序并行化是为了获取更高的执行效率,但是效率不能以牺牲正确性为代价。如果程序并行化后,连基本的执行结果的正确性都无法保证,那么并行程序苯本身就是去了意义。
还记得上一章中我们写了一个10个线程分别执行10000此i++的例子,我们期望的结果是1000000,但是结果却不像我们想的那样。因为可能多个线程同时对i进行写入时,其中一个线程的记过会覆盖另一个。
当时我们提出了两个解决方法,一个是加关键字synchronized,另一个是使用整型原子类。上一章中,我们已经给出了原子类的源码,下面请看加关键字synchronized源码:
package com.jingchu.thread.synchronize;
/**
* @description: 线程安全测试案例
* @author: JingChu
* @createtime :2020-07-20 17:52:17
**/
public class PlusTask implements Runnable {
private static int i = 0;
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
synchronized (PlusTask.class) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int j = 0; j < 10; j++) {
threads[j] = new Thread(new PlusTask());
threads[j].start();
}
for (int j = 0; j < 10; j++) {
threads[j].join();
}
System.out.println(i);
}
}
运行结果:
关键字synchronized有多种用法,这里做一个简单的整理
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁
- 直接作用于实例方法:相当于给实例方法加锁,进入同步代码前要获得当前实例的锁
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获取当前类的锁
上面例子相当于给实例代码加锁
并发情况下的不易发现的错误
并发下的ArrayList
代码
我们都知道ArrayList是线程不安全的容器,那么到底会导致什么样的问题呢?请看如下代码:
package com.jingchu.thread.error;
import com.jingchu.thread.communication.MyWait;
import java.util.ArrayList;
/**
* @description: 并发下的ArrayList案例
* @author: JingChu
* @createtime :2020-07-20 18:03:44
**/
public class MyArrayList {
static ArrayList<Integer> arrayList = new ArrayList<>(10);
public static class AddThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
arrayList.add(i);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AddThread());
Thread t2 = new Thread(new AddThread());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(arrayList.size());
}
}
运行结果1:
这个出现了一个非常隐蔽的错误,就像之前多个线程进行i++类似。
运行结果2:
这个结果说明程序抛出了异常,这是因为ArrayList在扩容的过程中,内部的一致性被破快,但由于没有锁的保护,另外的线程访问到了不一致的内部状态,导致出现了越界问题
运行结果3:
这个结果说明,ArrayList的最终大小确实为20000,这说明并发程序有问题,但不一定每次都表现出来
解决方法很简单,使用线程安全的Vectory代替ArrayList即可
并发下的HashMap
HashMap同样不是线程安全的。当你使用多线程访问HashMap时,也能回遇到意想不到的错误,不过和ArrayList不同,HashMap的问题似乎更加诡异。
代码
package com.jingchu.thread.error;
import java.util.HashMap;
import java.util.Map;
/**
* @description: 并发下的HashMap测试案例
* @author: JingChu
* @createtime :2020-07-20 18:12:50
**/
public class MyHashMap {
static Map<String, String> map = new HashMap<>();
public static class AddThread implements Runnable {
int start = 0;
public AddThread(int start) {
this.start = start;
}
@Override
public void run() {
for (int i = start; i < 10000; i = i + 2) {
map.put(Integer.toString(i), Integer.toBinaryString(i));
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AddThread(0));
Thread t2 = new Thread(new AddThread(1));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(map.size());
}
}
结果
结果 | 内容 |
---|---|
① | 10000 |
② | 小于10000 |
③ | 程序无法停止 |
对于前两种情况和ArrayList一致,无需多解释,但是第三种,大家一定很惊讶。
分析
打开任务管理器,你们会发现这段代码占用了极高的CPU,可能已经占满了两个CPU。这非常类似于死循环的情况。
当两个线程在遍历HashMap的数据,是一个迭代遍历,就如同在遍历一个列表,由于多线程的冲突,链表的结构遭到了破坏,上述迭代就如同一个死循环。
错误的加锁
初学时,经常出现这个问题。下面请看下面的代码
package com.jingchu.thread.error;
/**
* @description: 错误加锁问题
* @author: JingChu
* @createtime :2020-07-20 18:26:23
**/
public class MyBadnLocak {
public static class BadnLocak implements Runnable {
public static Integer i = 0;
static BadnLocak instance = new BadnLocak();
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized (i){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
}
我们为了保证技术器i的正确性,每次都i自增前,都先获得i的锁,为了保证i是线程安全的。但是执行结果却不如同我们的预期,我们获得的都是小与20000的值。
解释:
因为Integer属于不变对象,也就是说对象一旦创建了,就不可能被修改。也就是说,如果你有一个Integer代表1,那么它韵苑就是1,你不可能改变Inter的值,使他变成2,如果要使用2怎么办呢,就仙剑了一个Integr,并让他表示2,我们查看Integer.valueOf()源码,如下图。
实际上他是一个工厂方法,它倾向于返回一个代表数值的Integer实例。
如此一来我们也就清楚了,由于多线程间,并不一定能看到同一个对象,因此,两个线程每次加锁都加在了不同的对象实例上,导致了外面的代码出现了问题。