Java多线程相关知识。
Java给多线程变成提供了内置的支持。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
进程:一个进程包括由操作系统分配的内存空间,包括一个或多个线程。一个线程不能独立存在,他必须是进程的一部分。一个进程一直运行,知道所有非守护线程都结束运行后才能结束。
一个线程的生命周期
- 新建状态:创建线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()这个线程。
- 就绪状态:线程对象调用start()方法后,进入就绪状态,处于就绪队列中,等到JVM里的线程调度器的调度。
- 运行状态:就绪状态的线程获得CPU资源,就可以执行run(),处于运行状态。运行状态可以变为阻塞状态,就绪状态或死亡状态。
- 阻塞状态:线程执行sleep睡眠,suspend挂起等方法,失去所占用资源后,进入阻塞状态。在睡眠到时或获得设备资源后重新进入就绪状态。
- 等待阻塞 :执行wait()
- 同步阻塞 :线程在获取synchronized同步锁失败(同步锁被其他线程占用)
- 其他阻塞 :通过调用sleep()或join()发起了IO请求。
- 死亡状态:运行状态的线程完成任务或其他终止条件发生时,就切换到终止状态。
Java线程的优先级
每个线程都有优先级,有助于操作系统确定线程的调度顺序:
Java线程的优先级是一个整数 1(Thread.MIN_PRIORITY)~10(Thread.MAX_PRIORITY)
默认情况下,线程都会去分配一个NORM_PRIORITY(5)。
注意线程优先级并不能保证线程执行的顺序,而且非常依赖与平台。
Java创建一个线程的三种方式
- 继承线程类 Thread (可以使用匿名类)
- 实现Runnable接口,线程类只是实现了Runnable接口,还可以继承其他类。
- 通过Callable和Future创建线程
1 | 实现Runnable接口 |
继承Thread来创建线程。继承类比如重写run()方法,该方法是新县城的入口点,必须调用start()才能执行。本质上也是实现了Runnable接口。
1 | class ThreadDemo extends Thread { |
Thread类的重要方法:
- public void start()
- public void run() 如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的方法;否则该方法不执行任何操作并返回。
- public final void setName(String name)
- public final void setPriority(int priority)
- public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程
- public final void join(long millisec)
- public void interrupt()
- public final boolean isAlive()测试线程是否处于活动状态
- public static void yield()暂停当前执行的线程对象,并执行其他线程
- public static void sleep(long millisec)
- public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回true
- public static Thread currentThread()
- public static void dumpStack()将当前线程的堆栈跟踪打印
加入线程join()。所有进程,至少会有一个线程为主线程,即main方法开始执行,就会有一个看不见的主线程存在。执行thread.join()就表明在主线程中加入该线程。主线程会等待该线程结束完毕才会往下运行。
守护线程
守护线程的概念是:当一个进程里,所有的线程都是守护线程的时候,结束当前进程。守护线程通常会被用来做日志,性能统计等工作。
线程的同步 Concurrency
所线程的同步问题指的是多个线程同时修改一个数据的时候可能导致的问题。
解决思路是,在增加线程访问一个数据的时候,其他线程不可以访问该数据。
synchronized同步对象概念
1 | Object someObject = new Object(); |
synchronized表示当前线程,独占对象someObject。当前线程独占了对象,如果有其他线程试图占有对象someObject,就会等待,直到该线程释放对象占用。someObject又叫同步对象,所有的对象都可以作为同步对象。注意这个对象不一定是要被修改的那个对象,只要是一个对象,所有的线程都去试图访问的一个对象就可以。
当然对一个对象来说可以这么写:
1 | m1和m2达到的效果是一样的 |
如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类。同一时间,只有一个线程能够进入 这种类的一个实例 去修改数据,从而保证了这个实例中的数据的安全。
1 | 属于线程安全类的 |
线程死锁
- 线程1首先占有对象1,接着试图占有对象2
- 线程2首先占有对象2,接着试图占有对象1
- 线程1等待线程2释放对象2,于此同时线程2等待线程1释放对象1
线程之间交互
使用wait和notify进行线程交互
this.wait表示让占有this的线程等待,并临时释放占有。调用wait是有条件的,必须是在synchronized块里,否则会出错。
this.notify表示通知哪些等待在this的线程可以苏醒过来了。
this.notifyAll()的意思是,通知一个所有等待在这个同步对象上的线程可以苏醒了。
需要强调的是wait和notify并不是Thread线程上的方法,他们是Object上的方法。因为所有的Object都可以被同来作为同步对象。
线程池
每个线程的启动和结束都是比较消耗时间和占用资源的。如果在系统中用了很多线程,大量的启动和结束动作会导致系统性能变卡,相应变慢。为了解决这个问题,引入线程池这种思想。
- 准备一个任务容器
- 一次性启动10个消费者线程
- 该开始任务容器是空的,所有的线程都wait
- 知道一个外部线程往这个任务容器中扔了一个任务,就会有一个消费者线程被notify唤醒
- 这个消费者线程取出任务,并执行这个任务,执行完毕后,继续等待下一次任务
- 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。
整个这个过程,都不需要创建新的线程,而是循环使用这些已经存在的线程
注意这个过程中的线程同步锁是这个任务容器。添加任务是向这个容器中添加任务,读取任务proceed也是从这个容器中提取。所以要保证该容器的读写唯一性。当容器为空时,所有的线程wait状态,等待任务容器存入一个任务后,唤醒所有线程notifyAll来proceed。
Java自建的线程池类ThreadPoolExecutor
使用Lock对象实现同步效果
Lock是一个接口,为了使用一个Lock对象,需要用到
Lock lock = new ReentrantLock();
与Synchronized(someObject)类似,lock方法表示当前线程占用lock对象,一旦占用,其他线程就不能占用了。
与Synchronized(someObject)不同的是,一旦Synchronized块结束,就会自动释放对someObject的占用。lock必须调用unlock方法进行手动释放。为了保证释放的执行,往往会把unlock()放在finally中进行。
Synchronized是不占用到手不会停止,会一直试图占用下去。Lock接口提供了一个trylock方法,trylock会在指定时间范围内试图占用,占用可能成功,也可能失败。在后边unlock释放的时候需要判断是否占用成功了。如果没有占用成功就会报错。
使用Synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll的方法。Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的await,signal,signalAll方法。
1 | Lock lock = new ReentrantLock(); |
区别:
- Lock是一个借口,而Synchronized是Java中的关键字,Synchronized是内置的语言实现,Lock是代码层面的实现。
- Lock可以选择性的获取锁,如果一段时间获取不到可以放弃。Synchronized不行,会一直取下去。Lock的这个特性可以避免死锁,Synchronized必须通过谨慎良好的设计才能减少死锁的发生。
- Synchronized在发生异常和同步快结束的时候会自动释放锁。而Lock必须手动释放。如果忘记释放一样会造成死锁。
原子性操作
原子性操作即为不可中断的操作,比如赋值操作 int i = 5;
原子性操作本身是线程安全的。但是对于i++这个行为,事实上是由3个原子性操作组成的,何在一起就不是线程安全的了。
JDK6之后,新增加了一个java.util.concurrent.atomic。里边包含了各种原子类。包含各种原子性操作,如自增,自减等方法。