JAVA多线程总结(一)

应用场景


(1) 异步处理,例如:发微博、记录日志等;

(2) 分布式计算

(3) 定期执行一些特殊任务:如定期更新配置文件,任务调度(如quartz),一些监控用于定期信息采集等

(4) TOMCAT处理多用户请求。

(5) 针对特别耗时的操作。多线程同步执行可以提高速度。例如:定时向大量(100w以上)的用户发送邮件。

并发编程面临的挑战及解决思路

问题一:上下文切换。

并发不一定快于串行,因为会有切换上下文的开销。【切换上下文:单核并发时,cpu会使用时间片轮转实现并发,每一次轮转,会保留当前执行的状态】。

 解决上下文切换开销的办法:

 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

    ·CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

    ·使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

    ·协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

问题二:死锁。

死锁是一个比较常见也比较难解决的问题,当多个线程等待同一个不会释放的资源时,就会发生死锁。避免死锁可以参考下面的思路。

避免死锁的方法:

1. 避免一个线程同时获取多个锁。

2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。


问题三:资源限制。

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1M每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s

解决资源限制思路:

1. 对于硬件资源限制,可以考虑使用集群并行执行程序。

2. 对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。

基础示例

实现多线程基本的实现方式就是如下两种:

继承Thread类;

实现Runnable接口;

实际使用时,会用到线程池,还会用spring管理线程池,下面使用多线程完成几个小例子。

示例一:多线程使用reentrantLock实现交替打印奇数偶数,代码见压缩包:

JAVA多线程总结(一)_第1张图片

示例二:4个线程,两个存钱,两个取钱

JAVA多线程总结(一)_第2张图片

示例三:spring管理线程池配置

JAVA多线程总结(一)_第3张图片

停止线程

终止线程有三种方式:

(1)使用退出标志,run()执行完以后退出【抛出异常或者return】

(2)使用stop强行停止线程,不推荐,会导致当前任务执行到一半突然中断,出现不可预料的问题;而且stop和suspend以及resume一样是过期作废的方法

(3)使用interrupt中断线程

 

interrupt()方法不会真的停止线程,而是会记录一个标志,这个标志,可以由下面的两个方法检测到。

Thread.interrupted()测试当前线程是否停止,但是他具有清除线程中断状态功能,如第一次返回true,第二次调用会返回false

Thread.isInterrupted(),仅返回结果,不清除状态。重复调用会结果一致

基于上面的逻辑,可以根据标志来在run()里面状态,然后再使用interrupt()来使代码停止,停止代码可以使用抛出异常的方式。

 

如果在sleep里面抛出异常停止线程,会进入catch,并清除停止状态,使之变成false;

stop()暴力停止,已经被作废,建议不使用;

使用stop的方法带来的问题:1.执行到一半强制停止,可能清理工作来不及;

2.对锁定的对象进行了解锁,导致数据不同步,不一致。

 

return方法停止线程:

其实就是使用 打标记+return 替换 打标记+抛异常

 

# 暂停线程 与 恢复线程

suspend()暂停,resume()恢复,已经被弃用,

 缺点:

1. 独占,使用不当很容易让公共的同步对象独占,使得其他线程无法访问。

2. 不同步:线程暂停容易导致不同步。

yield():作用是放弃当前cpu资源,将他让给其他任务去占用cpu;但是放弃的时间不确定,有可能刚放弃,马上又获得cpu时间片;直接在run方法里面使用即可。

线程优先级

多个线程可以设置优先级。

优先级设置:

setPriority()方法;分为1-10 10个等级,超过这个范围,会抛出异常。

java线程优先级可以继承,A线程启动B线程,那么B与A的优先级是一样的。

优先级高的绝大多数会先执行,但结果不是百分之百的。

对象以及变量访问

在run里面执行的方法,如果是同步的,则不会有线程安全问题,使用synchronized关键字即可保证同步。

synchronized持有的锁是对象锁,如果多个线程访问多个对象,则JVM会创建多个锁。【多个对象,多个锁,此处对象是指加了synchronized关键字的方法所在的类也就是创建线程时传入的对象,例如:

Thread a = new Thread(object1);

Thread b= new Thread(object2);

a.start();

b.start();

这种情况下线程a和b持有的是两个不同的锁。

# 赃读:

读取全局变量时,此变量已经被其他线程修改过了,就会出现赃读。

# synchronized实际上是对象锁。

现有A,B两个线程,C对象,C拥有加了synchronized关键字的方法X1()和X2(),以及未加synchronized关键字的X3()方法。

当A线程访问X1方法时,B线程想访问X1,必须等待A执行完,释放对象锁;

当A在访问X1,B想访问X3(),无需等待,直接访问。

当A在访问X1,B想访问X2(),需要等待A执行完。

# synchronized锁重入

在synchronized方法内,调用本类的其他的synchronized方法时,总是可以成功。

如果不可重入的话,会造成死锁;

可重入锁,支持在父子类继承的环境:子类可以通过"可重入锁"调用父类的同步方法。

#异常会释放锁

当一个线程执行出现异常,会释放他所持有的所有锁。

#同步不具有继承性

父类中A()方法是synchronized的,子类中的A方法,不会是同步的,需要手动加上。

#synchronized同步语句块

synchronized(this){

   ...同步的代码块...

}

synchronized声明方法的弊端:

A线程调用同步方法执行长时间任务时,B线程需要等待很久。

解决办法:可以使用synchronized同步语句块。

synchronized可以修饰代码块。使用synchronized修饰需要保持同步部分代码,其余部分异步,借此提高运行效率。

#synchronized代码块间的同步性

A对象,拥有X1和X2两个synchronized同步代码块,

那么,B线程在访问X1时,C线程也无法访问X2,需要等待B线程释放对象锁。

此处与synchronized修饰方法时一样。他们持有的都是对象锁。

#任意对象作为监视器

   synchronized修饰的代码块时,如果传入this,则会监视当前对象,加锁时会对当前整个对象加锁;

    例如:

    对象A有方法X1(),X2(),如果在X1和X2里有一段同步代码块,并且synchronized(this)传入的都是this对象,那么在B线程访问X1的同步代码块时,C线程也无法X2的同步代码块。

如果传入的不是this,而是另外的对象,则C可以访问X2的同步代码块。

*要保证传入其他监视对象时的成功同步,必须保证在调用时,监视对象是一致的,不能每次都new一个监视对象,否则会导致变成异步的。*

#脏读问题

有时候,仅仅使用synchronized修饰方法,并不能保证正确的逻辑。

比如,两个synchronized修饰的方法add()与getSize(),他们分别是对list进行读与写的操作,此时两个线程先后调用这两个方法,会导致结果超出预期。

解决:

add()方法中,synchronized改成去修饰代码块,并且传入监视对象list;

synchronized(list){

   --- add  ---

}

 

#静态同步synchronized方法,与synchronized(class)代码块

   synchronized加在static静态方法上,就是对当前.java文件对应的class类进行持锁。

   synchronized static等同于synchronized (object.class) 可以对该类的所有对象起作用,

*即:即使需要new不同的对象,也可以保持同步*

#String的常量池特性

一般不使用String变量来作为锁的监视对象,当对一个String变量持有锁时,如果两个访问线程传入的String变量值一样,会导致锁不被释放,其中一个线程无法执行。

可以使用对象来存储相应的变量解决此问题。

volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

 

#volatile保证有序性

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可能上面说的比较绕,举个简单的例子:

//x、y为非volatile变量

//flag为volatile变量

x = 2;        //语句1

y = 0;        //语句2

flag = true;  //语句3

x = 4;         //语句4

y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

那么我们回到前面举的一个例子:

//线程1:

context =loadContext();   //语句1

inited = true;             //语句2

//线程2:

while(!inited ){

  sleep()

}

doSomethingwithconfig(context);

前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

 

 

#与synchronized对比

1. volatile是线程同步的轻量实现,只能修饰变量,性能高于synchronized

2. volatile保证可见性,不保证原子性【一旦其修饰的变量改变,其余的线程都能发现,因为会强制从公共堆栈取值】,synchronized保证原子性,间接保证可见性,因为他会将私有内存和公共内存的值同步

例如:i++操作,实际上不是原子操作,他有3步:

(1).从内存取i值

(2).计算i的值

(3).将i的新值写到内存

多个线程执行时,使用volatile,可能导致数据脏读,进而出现错误。

3. 多线程访问volatile不会阻塞,而synchronized会

4. volatile是解决变量在多个线程之间的可见性,synchronized是保证多个线程之间资源的同步性。

 

# volatile的实现原理

1.可见性

处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

2.有序性

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

# volatile的应用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

下面列举几个Java中使用volatile的几个场景:

①     .状态标记量

volatile booleanflag = false;

 //线程1

while(!flag){

    doSomething();

}

  //线程2

public voidsetFlag() {

    flag = true;

}

根据状态标记,终止线程。

②.单例模式中的doublecheck

class Singleton{

    private volatile static Singleton instance= null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        if(instance==null) {

            synchronized (Singleton.class) {

                if(instance==null)

                    instance = new Singleton();

            }

        }

        return instance;

    }

}

为什么要使用volatile 修饰instance?

主要在于instance= new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

 

1.给 instance分配内存

2.调用Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

线程间通信

# 1.等待通知机制:

 wait使线程暂停,而notify使线程继续运行。还有notifyAll()方法。

 wait()和notify(),两个方法来实现等待通知机制;

 注意:(1)两个方法在调用时都需要持有当前对象的对象锁,所以都只能在同步代码块或者同步方法里面调用,如果不是会抛出异常。

# wait:

 (2)wait方法会将当前线程置入“预执行队列”,并在wait()所在代码行停止执行,直到接到notify(),或者被中断;

 (3)执行wait()后,当前线程释放锁;

#notify:

(1)如果多个线程在wait,那么会由线程规划器,挑选一个执行notify,并使他获取该对象的对象锁;

(2)noitfy执行之后,当前线程不会立马释放该对象锁,wait状态的线程也不能立马获得该对象锁,要等执行notify()方法的线程将程序执行完,也就是退出synchronized代码块之后才会释放锁,并让wait获得。

(3)多个wait的线程,第一个获取到notify并执行完之后,其余的wait状态的线程如果没有被通知,还是会一直阻塞。

#wait之后自动释放锁,notify之后不会立马释放锁

 

#interrupt方法与wait

当线程在wait状态时,调用对象的interrupt()方法,会抛出异常。

(1)执行完同步代码块之后,会释放当前对象的锁

(2)执行同步代码块过程中,抛出异常也会释放锁

(3)执行wait()之后,也会释放锁

#wait(long)

执行wait(5000)后,首先会等待5秒,如果5秒内没有收到通知,会自动唤醒线程,退出wait状态。

#通过管道进行线程间通信

4个类进行线程间通信:

(1)字节流:PipedInputStream和PipedOuputStream

(2)字符流:PipedReader和PipedWriter

使用语法:

输出:PipedOuputStream

PipedOuputStream out;

out.write();

out.close();

#join方法

在主线程中调用子线程的join方法,可以让主线程等待子线程结束之后,

再开始执行join()之后的代码。

join可以使线程排队运行,类似于synchronized的同步;区别在于join在内部使用wait()等待,而synchronized使用对象监视器原理同步。

#注意

在join过程中,如果当前线程对象被中断,则当前线程出现异常,子线程会继续运行;

#join(long)

long参数是设定等待时间,使用sleep(long)也可以等待,但二者是有区别的:

join(long),内部是使用的wait(long),等待时会释放锁;

sleep(long)等待时不会释放锁。

#ThreadLocal

变量值的共享可以使用public static;

如果想让每个线程都有自己的共享变量。可以使用ThreadLocal;ThreadLocal可以看做全局存放数据的盒子,盒子中可以存储每个线程的私有数据;

使用时,只需新建一个类继承ThreadLocal即可实现,不同的线程在这个类中取到各自隔离的变量。

#InheritableThreadlocal

InheritableThreadlocal可以在子线程中取得父线程继承下来的值。

使用注意:如果子线程取得值的同时,主线程将值进行了修改,那么取到的还是旧值。


LOCK的使用

ReenTrantLock可以和synchronized一样实现多线程之间的同步互斥,ReenTrantLock类在功能上还更加强大,有嗅探锁定,多路分支通知等。

使用:

privateLock lock =  new ReenTrantLock();

try{

//加锁

lock.lock();

//解锁

lock.unlock();

}catch{

}

#ReenTrantLock结合Condition实现等待/通知

功能上与synchronized结合wait/notify一样,而且更加灵活;

    一个Lock对象可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的condition中,从而可以有选择性的进行线程通知,在线程调度上更加灵活。

而在wait/notify时,被通知的线程是JVM随机选择的,不如ReenTrantLock来得灵活。

synchronized相当于整个lock对象中只有一个单一的condition,所有的线程都注册在它上面,线程开始notify时,需要通知所有的waitting线程,没有选择权,效率不高。

#使用

使用之前,必须使用lock.lock()获取对象锁。

privateCondition condition = lock.newCondition();

try{

    condition.await();

}catch{}

其实使用上wait()/notify()/notifyAll()相当于Condition类

里面的await()/signal()/signalAll()

wait(longtimeout)相当于await(long time,TimeUnit unit)


















你可能感兴趣的:(java)