1 多线程
1.1 什么是进程?什么是线程?
进程是一个应用程序。(一个进程是一个软件)
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。
1.2 对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。
回先启动JVM,而JVM就是一个进程。
JVM在启动一个主线程调用main方法。
同时在启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。
1.3 进程和线程是什么关系?举个例子
阿里巴巴:进程
马云:阿里巴巴的一个线程。
童文红:阿里巴巴的一个线程。
京东:进程
强东:京东的一个线程。
妹妹:京东的一个线程。
进程可以看做是现实生当中的公司。
线程可以看做是公司当中的某个员工。
注意:
进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的)
魔兽游戏是一个进程
酷狗音乐是一个进程
这两个进程是独立的,不共享资源。
线程A和线程B呢?
在java语言中:线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
火车站,可以看做是一个进程。
火车站中的每一个售票窗口可以看做是一个线程。
我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
所以多线程并发可以提高效率。
java中之所以有多线程机制,目的就是为了提高程序的处理效率。
1.4 思考一个问题:
使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束。
main方法结束之时主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。
1.5 分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
对于多核的CPU电脑来说,真正的多线程并发是没问题的。
4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。
什么是真正的多线程并发?
t1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。
单核的CPU表示只有一个大脑:
不能够做到真正多线程并发,但是可以做到给人一种“多线程”并发的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,
给人的感觉是:多个事情同时在做。
线程A:播放音乐。
线程B:运行魔兽游戏。
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,给我们的感觉是同时并发的。
电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后,人类的眼睛产生了错觉,感觉是动画的。
这说明人类的反应速度很慢,就像一根钢针扎到手上,到最终感觉到疼,这个过程是需要“很长的”时间的,在这个期间计算机可以进行亿万次的循环。
所以计算机的执行速度很快。
1.6 java语言中,实现线程有两种方式,哪两种方式呢?
第一种方式:编写一个类,直接机场java.lang.Thread,重写run方法。
// 定义线程类
public class MyThread exetends Thread{
public void run(){
}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程
t.start();
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
// 定义一个可运行的类
public class MyRunnable implements Runnable{
public void run(){
}
}
// 创建一个线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更灵活。
1.7 关于线程对象的生命周期
新建状态
就绪状态
运行状态
阻塞状态
死亡状态
1.8 (这部分内容属于了解)关于线程的调度
1.1 常见的线程调度模型有哪些?
抢占式调度模型:
哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
java采用的就是抢占式调度模型。
均分是调度模型:
平均分配CPU时间片,每个线程占有的CPU时间片长度一样,平均分配,一切平等。
有一些编程语言,线程调度模型才用的是这种方式。
1.2 java中提供了哪些方法是和线程调度有关系的呢?
实例方法
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程的优先级
最低优先级1
默认优先级是5
最高优先级是10
优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)
静态方法
static void yield() 让位方法。
暂停当前正在执行的线程对象,并执行其他线程。
yield()方法不是祖册方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
实例方法:
void join()
合并线程
class MyThread1 extends Thread{
public void doSome(){
MyThread2 t = new MyThread2();
t.join();// 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
}
}
calss MyThread2 extends Thread{
}
2 关于多线程并发环境下,数据的安全问题。
2.1 为什么这个是重点?
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。
这些代码我们都不需要编写。
最重要的事:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点:*****)
2.2 什么时候数据在多线程并发的环境下会存在安全问题呢?
三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。
2.3 怎么解决线程安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
线程排队执行。(不能并发)
用排队执行解决线程安全问题。
这种机制被称为:线程同步机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
怎么解决线程安全问题呀?
使用“线程同步机制”。
线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,
只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事。
2.4 说到线程同步这块,涉及到这两个专业术语:
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型(效率较高)
异步就是并发。
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,
这就是同步编程模型。效率较低,线程排队执行。
同步就是排队。
3 Java中有三大变量:[重要的内容]
实例变量: 在堆中
静态变量:在方法区
局部变量:在栈中
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈)
局部变量在栈中,所以局部变量永远都不会共享。
实例变量在堆中,堆只有一个。
静态变量在方法区中,方法区只有一个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量 + 常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
4 如果使用局部变量的话:
建议使用:StringBuilder
因为局部变量不存在线程安全问题。选择StringBuilder。
StringBuffer效率比较低。
ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
HashTable是线程安全的。
5 总结
synchronized有两种写法:
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是一把锁。
6 聊一聊,我们以后开发中应该子呢么解决线程安全问题?
是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑常见多个对象,这样实例变量内存就不共享了。(一个线程对应一个对象,100个线程对应100个对象。
对象不共享,就没有数据安全问题了。)
第三种:如果不能使用局部变量,对象也不能创建多个,这个时候只能选择synchronized了。线程同步机制。
7 线程这块还有哪些内容呢?列举一下:
7.1 守护线程
java语言中线程分为两大类:
一类是:用户线程。
一类是:守护线程(后台线程)
其中具有代表行的就是:垃圾回收线程(守护线程)。
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
每天00:00点的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,每到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
7.2 定时器
定时器的作用:
间隔特定的时间,执行特定的程序。
每周要进行银行账户的总账操作。
每天要进行数据的备份操作。
在实际的开发汇总,没隔多久执行一段特定的程序,这种需求是很常见的,那么在java中其实可以采用多种睡眠方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间醒来,执行任务,这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中很少用,因为现在有很多高的框架都是支持定时任务的。
在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单地配置,
就可以完成定时器的任务。
7.3 实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性。)
这种方式实现的线程可以获取线程的返回值。
之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。
思考:
系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么拿到这个执行结果呢?
使用第三种方式:实现Callable接口方式。
7.4 关于Object类中的wait和notify方法。(生产者和消费者模式。)
第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。
wait方法和notify方法不是通过线程对象调用的,
不是这样的:t.wait(),也不是这样的:t.notify .. 不对。
第二:wait()方法作用?
Object o = new Object();
o.wait();
表示:让正在o对象上活的线程进入等待状态,无期限等待,直到被唤醒为止。
o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。
第三:notify()方法作用?
Object o = new Object();
o.nodify();
表示:
唤醒正在o对象上等待的线程。
还有一个nodifyAll()方法:
这个方法是唤醒o对象上处于等待的所有线程。
一个线程一个栈:
案例1的内存图:
案例2:线程的run内存图
案例2:线程的start内存图:
案例1 猜猜一下代码中有几个线程???:
package com.javaSe.Thread; /* 大家分析以下程序,有几个线程?除了垃圾回收线程之外。有几个线程? 1个线程(因为程序只有一个栈) main begin m1 begin m2 begin m3 execute m2 end m1 end main end 一个栈中,自上而下的顺序依次逐行执行。 */ public class ThreadTest01 { public static void main(String[] args) { System.out.println("main begin"); m1(); System.out.println("main end"); } public static void m1(){ System.out.println("m1 begin"); m2(); System.out.println("m1 end"); } public static void m2(){ System.out.println("m2 begin"); m3(); System.out.println("m2 end"); } public static void m3(){ System.out.println("m3 execute"); } }
案例2 Thread线程:
package com.javaSe.Thread; /* 实现线程的第一种方式: 编写一个类,直接继承java.lang.Thread,重写run方法。 怎么创建线程对象?new就行了 怎么启动线程呢?调用线程对象的start()方法 注意: 亘古不变的道理: 方法体当中的代码永远都是自上而下的顺序依次逐行执行的。 以下程序的输出结果有这样的特点: 有先有后。 有多有少。 这是怎么回事呢???? */ public class ThreadTest02 { public static void main(String[] args) { // 这里是main方法,这里的代码属于主线程,在主栈中运行。 // 新建一个分支线程对象。 MyThread t = new MyThread(); // 启动线程 // t.run();// 这样子做的话不会启动线程,不会分配新的分支栈。不能并发,也就是说他还是一个单线程,run方法中的程序走完了,main方法的程序才会继续执行。 // 启动线程 // start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。 // 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程就启动成功了。 // 启动成功的线程会自动调用run()方法,并且run方法在分支栈的站底部(压栈)。 // run方法在分支栈的站底部,main方法在主栈的栈底部。run和main是平级的。 t.start(); // 这里的代码还是运行在主线程中 for (int i = 0; i < 100; i++) { System.out.println("主线程--->" + i); } } } class MyThread extends Thread{ @Override public void run() { // 编写程序,这段程序运行在分支线程中(分支栈)。 for(int i = 0; i < 1000; i++){ System.out.println("分支线程--->" + i); } } }
案例3 实现Runnable线程接口:
package com.javaSe.Thread; /* 实现线程的第二种方式:编写一个类实现java.lang.Runnable接口。 */ public class ThreadTest03 { public static void main(String[] args) { /* // 创建一个可运行对象 MyRunnable r = new MyRunnable(); // 将可运行的对象封装成一个线程对象 Thread t = new Thread(r);*/ // 将上面两行代码合并成一行 Thread t = new Thread(new MyRunnable()); // 启动线程 t.start(); // 这里的代码还是运行在主线程中 for (int i = 0; i < 100; i++) { System.out.println("主线程--->" + i); } } } // 这并不是一个线程类,是一个可运行的类,他还不是一个线程。 class MyRunnable implements Runnable{ @Override public void run() { // 编写程序,这段程序运行在分支线程中(分支栈)。 for(int i = 0; i < 1000; i++){ System.out.println("分支线程--->" + i); } } }
案例4 利用匿名内部类实现线程:
package com.javaSe.Thread; /* 采用匿名内部类可以吗? */ public class ThreadTest04 { public static void main(String[] args) { // 创建线程对象,采用匿名内部类方式。 Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("分支线程---> " + i); } } }); t.start(); for (int i = 0; i < 100; i++) { System.out.println("主线程---> " + i); } } }
案例5获取线程的名字:
package com.javaSe.Thread; /* 1 怎么获取当前线程对象 2 获取线程对象的名字 String name = 线程对象.getName(); 3 修改线程对象的名字 线程对象.setName("tttt"); 4 当线程没有设置名字的时候,默认的名字有什么规律?(了解一下) Thread-0 Thread-1 Thread-2 Thread-3 ... */ public class ThreadTest05 { public static void main(String[] args) { // 创建线程对象 MyThread2 mt = new MyThread2(); // 设置线程的名字 mt.setName("t1"); // 获取线程的名字 String mtName = mt.getName(); // 如果没有设置线程的名字 默认为:Thread-0 System.out.println(mtName); // 在新建一个线程 MyThread2 mt2 = new MyThread2(); mt2.setName("t2"); String mt2Name = mt2.getName(); System.out.println(mt2Name); // Thread-1 // 启动线程。 mt.start(); } } class MyThread2 extends Thread { public void run(){ for (int i = 0; i < 100; i++) { System.out.println("分支线程--->" + i); } } }
案例6:关于线程的sleep方法
package com.javaSe.Thread; /* 关于线程的sleep方法: static void sleep(long millis) 1 静态方法 2 参数是毫秒 3 作用:让当前线程进入休眠装填,进入阻塞状态,放弃占有的CPU时间片,让给其它线程使用。 这行代码出现在A线程中,A线程就进行休眠。 这行代码出现在B线程中,B线程就进行休眠。 4 Thread.sleep()方法,可以做到这种效果: 间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。 */ public class ThreadTest06 { public static void main(String[] args) { /*try { // 让当前线程进入休眠,睡眠5秒钟。 // 当前线程是主线程。 Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Hello World!");*/ for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); try { // 睡眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
案例7:关于sleep线程方法的面试题:
package com.javaSe.Thread; /* 关于线程的sleep方法: static void sleep(long millis) 1 静态方法 2 参数是毫秒 3 作用:让当前线程进入休眠装填,进入阻塞状态,放弃占有的CPU时间片,让给其它线程使用。 这行代码出现在A线程中,A线程就进行休眠。 这行代码出现在B线程中,B线程就进行休眠。 4 Thread.sleep()方法,可以做到这种效果: 间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。 */ public class ThreadTest06 { public static void main(String[] args) { /*try { // 让当前线程进入休眠,睡眠5秒钟。 // 当前线程是主线程。 Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Hello World!");*/ for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); try { // 睡眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
案例8 如何让正在睡眠的线程执行:
package com.javaSe.Thread; /* sleep睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么唤醒正在睡眠的线程。 注意:这个不是终断线程的执行,是终止线程的睡眠。 */ public class TheadTest08 { public static void main(String[] args) { // 创建线程对象 Thread t = new Thread(new MyRunnable2()); // 更改线程名称 t.setName("t"); // 启动线程 t.start(); // 希望5秒之后,t线程醒来(5秒之后主线程手里的活干完了) try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { // 打印异常信息 e.printStackTrace(); } // 终断t线程的睡眠(这段终断睡眠的方法依靠了java的异常处理机制。) t.interrupt();// 干扰,一盆冷水过去! } } class MyRunnable2 implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "---> begin"); try { // 睡眠1年,1年之后才可以醒来。 // 这里只能try/catch不能throws是因为什么呢? // 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。 Thread.sleep(1000 * 60 * 60 * 24 * 365); } catch (InterruptedException e) { e.printStackTrace(); } // 1年之后才会执行这里。 System.out.println(Thread.currentThread().getName() + "---> end"); // 调用doOther /*try { doOther(); } catch (Exception e) { e.printStackTrace(); }*/ } // 这个是可以的,你可以在其他方法进行throws,但是在run()方法中还是只可以进行try/catch】 /*public void doOther() throws Exception{ }*/ }
案例9 怎么强行终止一个线程(此方法现已不可以用,下面的案例才是最完美的):
package com.javaSe.Thread; /* 在java中怎么强行终止一个线程的执行。 这种方式存在很大的缺点:容易丢失数据,因为这种方式是直接将线程杀死了。 线程没有保存的数据将会丢失。不建议使用。 */ public class ThreadTest09 { public static void main(String[] args) { Thread t = new Thread(new MyRunnable3()); t.setName("t"); t.start(); // 模拟五秒钟 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } // 5秒之后强行终止t线程 t.stop();// 已过时(不建议使用) } } class MyRunnable3 implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
案例10 如何合理的终止一个线程:
package com.javaSe.Thread; /* 怎么合理的终止一个线程的执行,这种方式很常用的: */ public class ThreadTest10 { public static void main(String[] args) { MyRunnable4 r = new MyRunnable4(); Thread t = new Thread(r); t.setName("t"); t.start(); // 模拟5秒 try { t.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } // 终止线程 // 你想要什么时候终止t的执行,那么你把标记修改成false,就结束了。 r.run = false; } } class MyRunnable4 implements Runnable{ // 打一个布尔标记。 boolean run = true; @Override public void run() { for (int i = 0; i < 10; i++) { if (run){ System.out.println(Thread.currentThread().getName() + "--->" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } else { // return就结束了,你在结束之前还有什么没有保存的 // 在这里进行保存就可以了 save... // 终止当前线程 return; } } } }
案例11 如何获取线程的优先级:
package com.javaSe.Thread; public class ThreadTest11 { public static void main(String[] args) { /*System.out.println("最高优先级 = " + Thread.MAX_PRIORITY); System.out.println("最低优先级 = " + Thread.MIN_PRIORITY); System.out.println("默认优先级 = " + Thread.NORM_PRIORITY);*/ // 获取当前线程对象,获取当前线程的优先级 Thread currentThread = Thread.currentThread(); // 设置主线程的优先级1 currentThread.currentThread().setPriority(1); // main线程的默认优先级是5 // System.out.println(currentThread.getName() + "线程的默认优先级是" + currentThread.getPriority()); /*int priority = currentThread.getPriority(); System.out.println("当前对象线程优先级为 = " + priority);*/ Thread thread = new Thread(new MyRunnable5()); thread.setPriority(10); thread.setName("run"); thread.start(); // 优先级较高的,只是抢到的CPU时间片相对多一些。 // 大概率方向更偏向于优先级比较高的。 for (int i = 0; i < 10000; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); } } } class MyRunnable5 implements Runnable{ @Override public void run() { // 获取线程优先级 // System.out.println(Thread.currentThread().getName() + "线程的默认优先级" + Thread.currentThread().getPriority()); for (int i = 0; i < 10000; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
案例12 让位,当前线程暂停,回到就绪状态,让给其它线程
package com.javaSe.Thread; /* 让位,当前线程暂停,回到就绪状态,让给其它线程。 静态方法:Thread.yield(); */ public class ThreadTest12 { public static void main(String[] args) { Thread t = new Thread(new MyRunnable6()); t.setName("t"); t.start(); for (int i = 1; i <= 10000; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); } } } class MyRunnable6 implements Runnable{ @Override public void run() { for (int i = 1; i <= 10000; i++) { // 每100个让位1次 if (i % 100 == 0){ Thread.yield(); // 当前线程暂停一下,让给主线程。 } System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
案例13 线程合并:
package com.javaSe.Thread; /* 线程合并 */ public class ThreadTest13 { public static void main(String[] args) { System.out.println("main begin"); Thread t = new Thread(new MyRunnable7()); t.setName("t"); t.start(); // 合并线程 try { t.join(); // t合并到当前线程中,当前线程受到阻塞,t线程执行直到结束。 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main end"); } } class MyRunnable7 implements Runnable{ @Override public void run() { for (int i = 0; i < 10000; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
多线程并发对同一个账户进行取款
1 错误的多线程案例 如果线程并发,不进行排队,那么当两个线程操作同一个对象,就会出现问题:
银行账户类:
package com.javaSe.threadsafe; /* 银行账户 使用线程同步机制,解决线程安全问题。 */ public class Account { // 账户 private String actno; // 余额 private double balance; // 实例变量 // 对象 Object obj = new Object(); // 实例变量(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。) public Account() { } public Account(String actno, double balance) { this.actno = actno; this.balance = balance; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } // 取款的方法 public void withdraw(double money){ double before = this.getBalance(); // 10000 double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); } }
账户线程类:
package com.javaSe.threadsafe; public class AccountThread extends Thread { // 两个线程必须共享同一个账户对象。 private Account act; // 通过构造方法传递过来账户对象 public AccountThread(Account act){ this.act = act; } public void run(){ // run方法的执行表示取款操作 // 假设取款5000 double money = 5000; // 取款 act.withdraw(money); System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款"+ money +"成功,余额 = " + act.getBalance()); } }
测试类:
package com.javaSe.threadsafe; public class Test { public static void main(String[] args) { // 创建账户对象(只创建一个) Account act = new Account("A-001",10000); // 创建两个线程对象 Thread t1 = new AccountThread(act); Thread t2 = new AccountThread(act); // 设置name t1.setName("t1"); t2.setName("t2"); // 启动线程取款 t1.start(); t2.start(); } }
2 使用线程同步机制,解决线程安全问题:
银行账户类:
package com.javaSe.threadsafe2; /* 银行账户 使用线程同步机制,解决线程安全问题。 */ public class Account { // 账户 private String actno; // 余额 private double balance; // 实例变量 // 对象 Object obj = new Object(); // 实例变量(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。) public Account() { } public Account(String actno, double balance) { this.actno = actno; this.balance = balance; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } // 取款的方法 public void withdraw(double money){ // 以下这几行代码必须是线程排队的,不能并发。 // 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。 /* 线程同步机制的语法是: synchronized (){ // 线程同步代码块。 } synchronized后面的小括号中传的这个“数据”是相当关键的。 这个数据必须是多线程共享的数据,才能达到多线程排队。 ()中写什么? 那要看你想让哪些线程同步。 假设t1 t2 t3 t4 t5,有五个线程 你只希望t1 t2 t3排队,t4 t5 不需要排队怎么办? 你一定要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。 这里的共享对象:账户对象 账户对象是共享的吗,那么这里this就是账户对象吧 不一定是this,这里只要是多线程共享的那个对象就行。 在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁) 100个对象,100把锁。1个对象1把锁 以下代码的执行原理? 1 假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。 2 假设t1先执行了,遇到了synchronized 这个时候会自动找“后面共享对象”的对象锁,找到之后并占有这把锁,然后执行同步代码块中的程序, 在程序执行过程中一直都是占有者把锁的。知道同步代码块代码结束,这把锁才会释放。 3 假设t1已经占有了这把锁,t2也遇到了synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块的外面等待t1的结束, 直到t1把同步代码块执行结束了,t1也会归还这把锁,此时t2终于等到这把锁,然后t2占有这这把锁之后,进入同步代码块中执行代码。 这样就达到了线程排队执行。 这里需要注意的事:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。 */ // Object obj2 = new Object(); // Object o = null; // synchronized (o) { // java.lang.NullPointerException // synchronized (obj2) { // 这样编写就不安全了,因为obj2是一个局部变量,他不是共享对象,第一个线程对象进来的时候会new一个obj对象,第二个线程进来还是会new一个,那就是多个了 // synchronized (obj) { // 这样也行 // synchronized (this) { // 这种才是最好的 // synchronized ("abc") { // "abc"在字符串常量池当中,只有一个 但是这样的话 所有的线程都需要进行等待,这样不行。 double before = this.getBalance(); // 10000 double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); // } } }
账户线程类:
package com.javaSe.threadsafe2; public class AccountThread extends Thread { // 两个线程必须共享同一个账户对象。 private Account act; // 通过构造方法传递过来账户对象 public AccountThread(Account act){ this.act = act; } public void run(){ // run方法的执行表示取款操作 // 假设取款5000 double money = 5000; // 取款 // synchronized (this){ // 这样不行,因为这个是线程对象,你new了两个线程对象,他就是两个内存地址,不存在线程共享。 synchronized (act){ act.withdraw(money); // 这种方式也可以,只不过你扩大了同步的范围,效率更低了。 } System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款"+ money +"成功,余额 = " + act.getBalance()); } }
测试类:
package com.javaSe.threadsafe2; public class Test { public static void main(String[] args) { // 创建账户对象(只创建一个) Account act = new Account("A-001",10000); // 创建两个线程对象 Thread t1 = new AccountThread(act); Thread t2 = new AccountThread(act); // 设置name t1.setName("t1"); t2.setName("t2"); // 启动线程取款 t1.start(); t2.start(); } }
实例方法应用锁类:
银行账户类:
package com.javaSe.threadsafe3; public class Account { // 账户 private String actno; // 余额 private double balance; // 实例变量 // 对象 Object obj = new Object(); // 实例变量(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。) public Account() { } public Account(String actno, double balance) { this.actno = actno; this.balance = balance; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } // 取款的方法 /* 在实例方法上可以使用synchronized吗?可以的。 synchronized出现在实例方法上,一定锁的是this。 没得挑。只能是this。不能是其它的对象了。 所以这种方式不灵活哦 另外还有一个缺点:synchronized 出现在实例方法上 表示这个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率较低。 所以这种方式不常用。 synchronized使用在实例方法上有什么优点? 代码写的少了。节俭了。 如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。 */ public synchronized void withdraw(double money){ double before = this.getBalance(); // 10000 double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); } }
账户线程类:
package com.javaSe.threadsafe3; public class AccountThread extends Thread { // 两个线程必须共享同一个账户对象。 private Account act; // 通过构造方法传递过来账户对象 public AccountThread(Account act){ this.act = act; } public void run(){ // run方法的执行表示取款操作 // 假设取款5000 double money = 5000; // 取款 act.withdraw(money); // 这种方式也可以,只不过你扩大了同步的范围,效率更低了。 System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款"+ money +"成功,余额 = " + act.getBalance()); } }
测试类:
package com.javaSe.threadsafe3; public class Test { public static void main(String[] args) { // 创建账户对象(只创建一个) Account act = new Account("A-001",10000); // 创建两个线程对象 Thread t1 = new AccountThread(act); Thread t2 = new AccountThread(act); // 设置name t1.setName("t1"); t2.setName("t2"); // 启动线程取款 t1.start(); t2.start(); } }