多线程技术
-
线程安全
-
安全问题分析(重点)
-
/*
* 售票案例
*/
class Ticket implements Runnable{
// 充当票数
private int num = 100;
// 实现run方法
public void run(){
while( true ){
// 保证num大于零的时候可以售票
if( num > 0 ){
System.out.println(Thread.currentThread().getName() +" 正在售出的票号:"+ num);
num--;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 创建实现类的对象
Ticket task = new Ticket();
// 创建线程
Thread t = new Thread( task );
Thread t2 = new Thread( task );
Thread t3 = new Thread( task );
Thread t4 = new Thread( task );
// 开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
上面的代码运行发现结果出现了很多错误数据:
解释错误数据出现的原因:
在程序中有多个线程在执行run方法。而在cpu执行多个线程的时候,只要线程将任何一个代码(指令)执行结束之后,cpu就可以切换到其他的线程上去执行。
总结多线程安全问题发生的原因:
本质原因是CPU在执行多线程程序的时候,任何一个线程执行完任何一个指令之后,CPU都可以切换到其他的线程上去执行。导致执行时的数据不一致。出现错误数据。
在Java代码中线程安全问题的原因:
-
必须有多线程。
-
多个线程它们在操作共享的数据。
-
多个线程对共享的数据进行修改。
-
多线程安全问题解决(重点+编码)
Java中针对多线程的安全问题,解决方案:同步。
同步理解:卫生间。
同步的作用:使用同步来保证当有一个线程在操作共享的资源的时候,其他线程必须等待当前这个线程操作结束之后,其他线程中某个线程才能去操作。
Java中的同步有两种方式:
1、同步代码块
2、同步方法
同步代码块格式:
synchronized( 任意对象 ){
需要被同步的代码
}
理解同步代码块:
synchronized 理解成卫生间上的门
任意对象可以理解成 门上的锁
同步在添加的时候,添加在操作共享数据的代码:
class Ticket implements Runnable{
// 充当票数
private int num = 100;
//定义一个成员变量,充当同步上的锁对象
private Object lock = new Object();
// 实现run方法
public void run(){
while( true ){
// 需要在if的外面添加同步代码块
// t1 t2 t3
// 任何线程进入同步代码块之前,需要在这里获取到锁对象(隐式获取锁)
synchronized( lock ){
// 保证num大于零的时候可以售票
if( num > 0 ){
System.out.println(Thread.currentThread().getName() +" 正在售出的票号:"+ num);
// t0
num--;
}
}
//在线程出同步的时候,这个线程会将持有的锁释放
}
}
}
说明:
任何线程需要进入同步的时候,需要先获取同步上的锁对象,只有获取到锁对象的线程才能进入到同步中去执行同步中的代码, 其他任何线程只要没有获取到锁对象,只能在同步外面等待,等待在同步中的线程出同步之后,其他线程中某个线程才能获取到锁进入到同步中。
-
同步细节
-
多线程中同步细节
-
如果程序中有多线程,添加了同步,但是安全问题依然存在,怎么解决?
1、查找同步的位置是否添加正确,同步需要添加在操作共享数据的代码上。
2、查询多个线程它们进入同步的时候使用的锁是否相同。
-
JDK中和同步相关类
在前面学习JDK中的相关类的时候,都提到某些类安全,某些不安全。
安全:多个线程操作这些类的时候,它们不会出现错误数据。相当于这些类中有同步。
不安全:多个线程操作这些类的时候,没有同步。
字符串缓冲区:
StringBuffer:它是线程操作安全的。
StringBuilder:它是多线程操作不安全的。如果程序中只有单个线程,建议优先使用StringBuilder。
集合类:
从JDK1.2之后的所有集合类都多线程操作不安全的。
Vector和Hashtable它们是线程安全的。但是以后我们在开发中即使需要线程安全的集合也不会使用Vector和Hashtable。
如果我们需要使用多线程操作安全的集合,请使用Collections中的方法,将JDK1.2之后的集合转成安全的集合:
-
同步利弊
我们使用多线程的目的是提高程序的效率。但是多个线程操作共享数据会有安全问题,我们就必须使用同步解决安全。
同步好处:它保证线程的安全。
同步弊端:降低程序的执行效率。
-
多线程中的其他问题
-
单例懒汉式的安全问题
-
单例设计模式
-
-
设计模式:它是解决某类问题的一套有效的方案(模版)。
单例设计模式解决的问题:
保证当前的对象在程序中只有一个(对象唯一)。
单例代码的书写步骤:
1、私有本类的构造方法
2、创建本类的对象
3、对外提供获取对象的方法
-
单例常用代码书写格式
1、饿汉式
class Single{
private Single(){}
private static Single instance = new Single();
public static Single getInstance(){
return instance;
}
}
2、懒汉式
class Single{
private Single(){}
private static Single instance = null;
public static Single getInstance(){
if( instance == null ){
instance = new Single();
}
return instance;
}
}
单例面试的问题:
-
单例解决的问题?保证对象唯一
-
程序中为什么添加static
-
懒汉式在多线程操作的时候有没有安全问题?
肯定有安全问题。在getInstance方法中的判断和赋值两行代码上操作共享的instance变量,这样在操作的过程中cpu会切换,可能会出现安全问题。解决方案:在判断和赋值代码上添加同步代码块。
-
添加同步效率降低能不能优化?
可以。
-
懒汉式的安全问题
/*
* 单例懒汉式的安全问题
*/
// 书写单例类
class Single{
private Single(){}
private static Single instance = null;
// 定义一个对象作为同步的锁对象
private static Object lock = new Object();
public static Single getInstance(){
/*
* 外层的判断的目的是为后续其他线程在获取单例对象的时候提供效率,
* 后续其他线程再调用getInstance方法的时候,由于instance变量中已经有对象
* 那么判断就不会成立,线程就不用进入到同步中。更不会判断同步的锁
*/
if( instance == null ){
// 判断和赋值有安全问题 ,需要使用同步
synchronized( lock ){
if( instance == null ){
instance = new Single();
}
}
}
return instance;
}
}
// 书写线程的任务类
class Demo implements Runnable{
public void run() {
System.out.println(Single.getInstance());
// t0 // t1
}
}
// 测试
public class SingleDemo {
public static void main(String[] args) {
Demo d = new Demo();
Thread t = new Thread(d);
Thread t2 = new Thread(d);
t.start();
t2.start();
}
}
-
多线程中的死锁问题
-
死锁介绍
-
死锁:一般情况是锁嵌套导致。当多个线程在执行任务的时候,需要获取的锁顺序不一致,导致出现无法获取锁的情况,而程序就停滞不再执行任何代码。
常见的案例:
Thread-0线程:先获取A锁,在获取B锁,才能去执行任务。
Thread-1线程:先获取B锁,在获取A锁,才能去执行任务。
可是出现了Thread-0刚刚获取到A锁的时候,正要获取B锁,单还没有获取到,CPU切换到Thread-1线程上, 这样导致Thread-1获取到B锁,这时Thread-0获取获取Thread-1已经持有的锁,而Thread-1获取Thread-0持有的锁。可是线程的任务没有执行完,没有出同步,这时锁是不会释放的。
死锁:开发中尽量避免。如果没有线程的安全问题,一定不要加锁,如果一个锁可以解决的问题,不要使用多个锁,更不要使用锁嵌套。
-
死锁演示
/*
* 死锁程序演示
*/
class DeadLock implements Runnable{
// 定义2个锁对
private Object lock_A = new Object();
private Object lock_B = new Object();
// 定义boolean类型的变量,目的是切换线程到不同的代码区域中去运行
public boolean flag = true;
public void run(){
// 判断flag标记,目的是让切换一个进入if一个进入else执行
if ( flag ){
synchronized (lock_A) {
System.out.println(Thread.currentThread().getName() +" if lock_A");
synchronized (lock_B) {
System.out.println(Thread.currentThread().getName() +" if lock_B");
// 任务
}
}
}else{
synchronized (lock_B) {
System.out.println(Thread.currentThread().getName() +" else ...... lock_B");
synchronized (lock_A) {
System.out.println(Thread.currentThread().getName() +" else ...... lock_A");
// 任务
}
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) throws InterruptedException {
DeadLock task = new DeadLock();
Thread t = new Thread( task );
Thread t2 = new Thread( task );
t.start(); // 开启Thread-0 线程
/*
* 在开启Thread-0之后,需要将flag修改为false,目的是让Thread-1进入run方法中的else中
* 在执行t.start()代码的时候是主线程在执行,也就是cpu在主线程上,虽然开启Thread-0,但cpu不一定会立刻切换到Thread-0线程上
* 如果cpu没有切换,那么cpu还在主线程上,就会执行 task.flag = false;结果就会将标记修改为false,
* 修改结束之后,不管cpu会不会切换,这时标记都是false,即使cpu切换到thread-0线程上,也只能进入else中。
* 所以我们需要让主线程在执行完t.start();之后休眠,目的是让cpu一定可以切换到thread-0线程上。
*/
Thread.sleep(1);
task.flag = false;
t2.start();
}
}
-
多线程(同步)的锁
-
锁和作用介绍
-
在Java中线程的安全问题需要同步解决。而同步要求上面必须有一个对象作为同步的锁对象。任何线程进入同步的前提是必须先获取到同步上锁对象。只有持有了同步上的锁的这个线程才能进入到同步中执行代码。
锁:它是就是一个线程进入同步的标记。而在Java中允许同步代码块上的锁是任意一个合法的Java对象。
锁的作用:通过锁来保证只能有一个线程在同步中。
-
同步代码块中的锁
同步代码块的格式:
synchronized( 任意对象 ){
被同步的代码
}
同步代码块:
任何线程进行同步的时候,都要隐式的获取锁,线程出同步的时候需要隐式的释放锁。只要线程在同步中,这个线程就会持有同步上的锁对象。
-
同步方法上的锁
同步方法:当一个方法中的所有代码都需要被同步的时候,这时我们可以将同步添加在方法上。这样的方法就称为同步方法。
任何线程进入这个方法先要获取同步方法上的锁,这样永远只会有一个线程进入方法中执行。一旦方法执行完,线程才会释放同步方法上的锁。
普通的方法怎么定义:
修饰符 返回值类型 方法名( 参数列表 ){
方法体
}
同步方法的定义:
修饰符synchronized 返回值类型 方法名( 参数列表 ){
方法体
}
方法分成静态和非静态:
非静态的方法如果可以运行,前提必须有这个方法所属类的那个对象。通过对象调用方法。
非静态的同步方法,它使用的锁是当前调用这个方法的那个对象。也就是 this
静态方法:它不需要对象,直接通过类名调用。
静态同步的方法,它使用的锁是当前这个静态方法所在的类对应的classs文件。也就是 类名.class
-
线程状态(记住)
-
异常在线程中的体现
/*
* 演示 异常发生在线程上
*/
class Demo3 implements Runnable{
public void run(){
for( int i = 0 ; i < 20 ; i++ ){
System.out.println(Thread.currentThread().getName() + "...." + i);
if( i == 10 ){
int x = 1/0;
}
}
}
}
public class ExceptionDemo {
public static void main(String[] args) {
Demo3 d = new Demo3();
Thread t = new Thread(d);
Thread t2 = new Thread(d);
t.start();
t2.start();
for( int i = 0 ; i < 20 ; i++ ){
System.out.println(Thread.currentThread().getName() + "--------------" + i);
}
}
}
上面的程序运行,发生异常:
以后我们使用的程序基本上都是多线程的程序:
后期我们一定要注意异常具体在那个线程上发生的。当某个线程上发生异常之后,如果没有捕获这个异常。那么异常所在的当前这个线程会停止运行,其他没有发生异常的线程依然会正常运行。
-
线程的高级应用
-
单生产单消费
-
生产消费模型介绍(重点)
-
-
生产消费模型:
有一个共同资源,有多个线程去操作,但是它们对资源的操作方式不同。
有部分线程它们负责给资源中存放数据,有部分线程从资源中获取数据。
把上述的这个模型我们称为生产消费模型。
-
生产消费代码编写(重点)
/*
* 实现单生产,单消费原始代码
*/
// 专门定义一个负责描述保存和取出线程的操作的资源类
class Resource {
// 定义成员变量,充当保存和取出的容器
private Object[] objs = new Object[1];
// 提供一个保存数据的方法
public void save( Object obj ){
// 将数据保存到容器中
objs[0] = obj;
System.out.println(Thread.currentThread().getName()+" 正在保存的数据是:"+objs[0]);
}
// 提供一个取出数据的方法
public void get(){
// 打印取出的数据
System.out.println(Thread.currentThread().getName() + " 正在取出的数据::::" +objs[0]);
objs[0] = null;
}
}
// 书写保存数据线程的任务
class Productor implements Runnable {
private Resource r ;
// 书写构造方法的目的是在明确线程的任务的时候,
// 可以通过构造方法告诉线程的任务它们操作的资源对象
public Productor( Resource r){
this.r = r ;
}
public void run() {
// 给资源中保存数据
while(true){
r.save( "苹果" );
}
}
}
// 书写取出数据的线程任务
class Consumer implements Runnable {
private Resource r ;
// 书写构造方法的目的是在明确线程的任务的时候,
// 可以通过构造方法告诉线程的任务它们操作的资源对象
public Consumer( Resource r){
this.r = r ;
}
public void run() {
// 从资源中取出数据
while(true){
r.get();
}
}
}
// 测试类
public class ThreadTest {
public static void main(String[] args) {
// 先要创建一个保存和取出操作的共同的资源对象
Resource r = new Resource();
// 创建线程的任务对象
Productor pro = new Productor( r );
Consumer con = new Consumer( r );
// 创建线程对象 ,这个线程负责保存数据
Thread t = new Thread( pro );
// 创建线程对象 ,这个线程负责取出数据
Thread t2 = new Thread( con );
// 开启线程
t.start();
t2.start();
}
}
-
同步加入(重点)
上面的代码在运行的时候,出现了下面的错误数据:
出现保存null的情况,是因为在程序:
保存过程是先给数组中存值,然后取出值打印提示保存信息。
在objs[0] = obj 执行完之后,如果cpu切换到取出的线程上,就会导致执行取出的程序,而一旦取出的程序取出完之后,将数组空间设置null之后,在切换到保存线程上,那么打印出来的数据就是null。
解决方案:
在保存的时候不能执行取出的代码, 或者是执行取出的时候,不能执行保存的代码。我们要想完成这样的需求,就可以使用Java中同步完成。
/*
* 单生产单消费 添加同步保证生产的时候不能消费,或者消费的时候不能生产。
*/
// 专门定义一个负责描述保存和取出线程的操作的资源类
class Resource {
// 定义成员变量,充当保存和取出的容器
private Object[] objs = new Object[1];
// 定义一个对象充当同步的锁
private Object lock = new Object();
// 提供一个保存数据的方法
public void save( Object obj ){
// 使用同步代码块
synchronized( lock ){
// 将数据保存到容器中
objs[0] = obj;
System.out.println(Thread.currentThread().getName()+" 正在保存的数据是:"+objs[0]);
}
}
// 提供一个取出数据的方法
public void get(){
synchronized( lock ){
// 打印取出的数据
System.out.println(Thread.currentThread().getName() + " 正在取出的数据::::" +objs[0]);
objs[0] = null;
}
}
}
-
等待唤醒分析和加入(重点)
上面的程序中加入了同步,保证存的时候不能取,取的时候不能存。但是程序运行依然有问题:
出现的问题:出现了一直保存,从来没有被取出,或者一直在取出,却没有被保存。
问题的原因:我们程序中根本没有保存能不能保存,就直接保存,也没有判断能不能取出,就直接取出数据。
问题的解决:我们应该在保存的时候,先判断容器中有没有数据,如果没有才保存,如果有呢?这时保存的线程应该等着,等到取出的线程取出之后,再保存。
如果在取出的时候,先判断容器有没有数据,如果没有数据,应该等着,等到保存完成在取出,如果有数据就可以直接取出。
我们需要让线程根据条件可能要等待,等待到对方操作完成之后自己在操作。
我们查询Thread类的API发现,等待和唤醒的功能放在Object类中。
线程的等待,或者线程的唤醒,线程自己不能操作自己。
Java中如果要让线程等待或者唤醒等待的线程:这时要求线程必须在同步中。
只要在同步中的线程,它就会持有同步的锁对象。也就是说,同步上的锁最清楚那个线程在同步中。于是同步的锁就有能力让持有自己的那个线程处于等待状态。
同步的锁对象最清楚自己让那个线程等待了,锁才能将处于等待的线程唤醒了。
既然等待和唤醒是锁的功能,而我们介绍同步代码块上的锁可以任意对象,那就说明任意对象都可以具备让线程等待或者唤醒线程的方法。任意对象都具备的方法就应该定义在Object类中。
/*
* 单生产单消费 添加线程的等待和唤醒机制
*/
// 专门定义一个负责描述保存和取出线程的操作的资源类
class Resource {
// 定义成员变量,充当保存和取出的容器
private Object[] objs = new Object[1];
// 定义一个对象充当同步的锁
private Object lock = new Object();
// 提供一个保存数据的方法
public void save( Object obj ) {
// 使用同步代码块
synchronized( lock ){
// 判断能否操作容器
if( objs[0] != null ){
// 判断成立 说明保存的线程不能保存,需要等待
try { lock.wait(); } catch( InterruptedException e ){ }
}
// 将数据保存到容器中
objs[0] = obj;
System.out.println(Thread.currentThread().getName()+" 正在保存的数据是:"+objs[0]);
// 在生产者保存完数据之后,需要叫醒(唤醒)消费的线程
lock.notify();
}
}
// 提供一个取出数据的方法
public void get(){
synchronized( lock ){
if( objs[0] == null ){
// 判断成立,说明没有数据,不能取出,需要等待
try { lock.wait(); } catch( InterruptedException e ){ }
}
// 打印取出的数据
System.out.println(Thread.currentThread().getName() + " 正在取出的数据::::" +objs[0]);
objs[0] = null;
// 消费结束,需要通知生产者线程
lock.notify();
}
}
}
-
多生产多消费(了解)
-
多生产多消费代码实现
-
在单生产单消费的基础上,我们多添加了一个生产的线程和一个消费的线程,结果运行程序,发现:
出现多次取出null的情况,或者多次保存没有取出的情况。
上述问题的原因:
是因为在保存的时候,保存一个线程,唤醒了另外一个保存线程,或者是消费的一个线程唤醒另外一个消费的线程。
解决方案:将保存和取出中的if判断修改为while循环,保证唤醒的同伴可以继续判断。
将if修改为while之后死锁了,原因是:唤醒之后继续判断导致没有可以继续执行的线程,线程全部处于wait状态。
解决方案:使用notifyAll唤醒所有线程。
-
JDK5锁和等待唤醒
-
Lock接口介绍
-
-
Lock接口使用
-
Condition接口介绍
-
Condition接口应用
-
多线程的细节
-
wait和sleep区别
-
-
同步能不能添加在run方法上
-
线程组
-
线程优先级
-
守护线程
-
定时器介绍