创建线程的两种方法
- 继承
Thread类 - 实现
runnable接口,将实现runnable接口的类作为Thread的参数来创建Thread类,实现runnable接口的类只能重写run方法
线程的数据共享
在实现多线程的类中创建 静态类 属性
在外部定义一个对象,并使用引用传递,将对象的引用赋值给实现多线程的类
详解 Thread 类
public thread()
创建一个线程
public thread(Runnable target)
用一个实现runnable方法的类作为参数,创建一个Thread类
public thread(String name)
创建一个thread类,并使用name作为此线程名称,如果不设置线程的名字,线程也会有默认的名字,从0到n递增。
public static Thread currentThread()
返回调用此方法的线程名称
public static void yield()
放弃当前线程的运行权限,让其他线程运行,当前线程进入就绪状态, 如果没有同级别或者更高级别的线程存在,则会立刻获得运行权限。
public static void sleep()
使调用此方法的线程睡眠。
public void start()
调用此方法后,JVM会调用此线程的run方法,经过调度后,运行此线程。
public void run()
此线程的线程体,线程的运行代码编写于此处。
public final void stop()
停止,并销毁线程,不推荐使用此方法,可能造成异常。
public void interrupt()
中断线程
public final void join()
等待调用此方法的线程死亡后,当前线程才可以执行。
public final void join(long millis)
等待调用此方法的线程思维,或者等待millis秒后,当前线程执行。
public final void setPriority(int newPriority)
设置线程的优先级。
public final void setDaemon(Boolean on)
设置当前线程是否为后台线程。
public final void checkAccess()
测试,当前线程是否可以修改调用此方法的线程。 没有返回值,如果没有修改的权限会抛出异常。
public void setName(String name)
设置线程的名字,如果不设置线程的名字,线程也会有默认的名字,从0到n递增。
public boolean isAlive()
判断线程是否存活。
线程间的互斥
同时运行的线程都需要使用某一个或某一些数据,而此数据某一时刻只能由一个线程访问。
监视器机制
Java的每个对象只有一个锁,通过各个线程对这个锁的竞争,来实现线程间的互斥。
监视区
某一段代码或数据某一时刻只能由一个线程访问,这一段代码或数据被称为监视区。
线程间的同步
多个线程有条件的同时操作共享数据,通过调度,每个线程都能单独的操作共享数据。
synchronized
线程同步关键字,通常用此关键字定义监视区,被此关键字定义的代码区会执行原子操作,但是此关键字定义的代码段执行效率比使用锁低。
// 写法一:
synchronized (obj){ // obj 为监视区数据
// code block
}
// synchronized 有锁的概念,如果当前obj的锁被使用,则等待锁被释放
//若obj的锁空闲,则获得锁,并执行代码块内的代码,代码执行完后,则释放锁。
// 写法二 同步方法
public synchronized void out(){
// 通过synchronized 的修饰,此方法就变成了原子操作
}
线程的等待与唤醒
线程的等待与唤醒是线程间的一种通信机制,java的object类有wait、notify和notifyall等方法
wait方法可以使调用此方法的线程进入等待
notify可以随机唤醒一个线程,唤醒后不会立刻执行,需要等待相关的监视器的释放。
notifyall 可以唤醒所有线程
后台线程
可以使用setdaemon方法来使某一个线程变成后台线程(需要在线程启动前设置,线程启动后无法从前台进程转化为后台进程),后台线程随class类这个进程的结束而结束。若存在一个前台线程,则进程未结束,若不存在前台线程,则进程结束,同时后台进程也结束。 垃圾回收机制,就是一个后台线程。
线程的生命周期
就绪状态
线程调用start方法后会变成就绪态。 线程休眠时间到后会变成就绪态。 lock式阻塞线程获得锁后会变成就绪态。
死亡状态
线程运行结束会变成死亡状态 线程调用stop方法会变成死亡状态
阻塞状态
线程调用wait方法会进入wait式阻塞状态 wait式阻塞状态的线程被唤醒后会进入lock式阻塞状态 线程调用sleep后会变成睡眠式阻塞状态 线程执行时,未获得锁会进入lock式阻塞状态 如果线程调度算法为优先级调度,当出现优先级高的线程时,当前线程会进入阻塞状态。 如果系统支持时间片调度且线程调度算法为时间片调度,当时间片时间用光了,会进入阻塞状态 线程调用yield方法时,会进入阻塞状态
运动状态
运动状态只能由就绪状态转变
线程的死锁
线程依赖形成环导致环上的所有线程都进入阻塞状态。 三角形同时拿两个小球问题。
线程的优先级
Java虚拟机支持一个简单的线程调度算法,即根据线程的优先级调度,优先级分为1到10级,实际上并没有这么多级,优先级数值越大,优先级越大。
线程的优先级可以通过setPriority方法修改。
在一个线程内创建一个线程,线程的优先级会被继承。
对于优先级相同的线程,Java的调度是随机的,可以使用yield方法放弃线程当前的执行权限。
获取运行权限的线程停止运行的原因: 时间片满,有优先级更高的线程,线程调用wait、yield、stop方法,需要等待某个锁时
当某个优先级高的线程使用yield方法放弃当前的运行权限时,会进入就绪状态,如果没有优先级和此线程相同或者更高的线程,则此线程会立刻夺回运行权限,优先级低的线程无法获得运行权限。
线程安全
如果一个对象的操作不需要添加任何其他操作且与其他线程仍是同步的,则这个对象被称为线程安全的对象。
- 同步集合:如
Vector、Hashtable等,它们通过在方法上添加synchronized关键字来实现线程安全。 - 并发集合:如
ConcurrentHashMap、CopyOnWriteArrayList等,它们通过使用高效的并发算法来实现线程安全。 - 阻塞队列:如
ArrayBlockingQueue、LinkedBlockingQueue等,它们通过使用锁或者其他同步机制来实现线程安全。
绝对线程安全
不管怎么使用都是线程安全的。
相对线程安全
单线程时安全,多线程时可以通过一些辅助手段来达到安全的目的。
线程兼容与互斥
线程兼容
对象不是线程安全的,但是在多线程环境中,可以通过一些操作使其变成线程安全的。
线程互斥
对象不是线程安全的,且在多线程环境中,无论如何操作,都不会变成线程安全的。
synchronized 关键字
使用synchronized关键字修饰的代码块, 在编译后的前后会产生monitorenter和monitorexit两个字节码,在当前线程没有执行完之前,其他线程无法使用这个代码块,当时对于当前的线程,此代码块是可重入的。
synchronized 实际上是一个重入锁(ReentrantLock),但是重入锁可以实现更多的功能,例如:等待可中断、公平锁、锁可以绑定多个条件
ReentrantLock表现为API层面的互斥锁,synchronized表现为语法层面的互斥锁。
线程安全的实现方式
互斥同步
使用互斥量,临界区,信号量
非阻塞同步
阻塞同步算力开销太大了,不建议使用。
非阻塞同步:线程需要某个锁时,使用轮询的方式检测此锁是否可用。
Java中有一些类实现了非阻塞同步,例如:AtomicInteger、AtomicDouble 等。
使用处理器指令进行不断重试策略(JDK 1.5 之后) 1、测试并设置(Test-and-Set) 2、获取并增加(Fetch-and-Increment) 3、交换(Swap) 4、比较并交换(Compare-and-Swap) 5、加载链接,条件存储(Load-Linked,Store-conditional)
无同步方案
可重入代码:执行一半后不执行,然后再执行,不会出现问题。 ThreadLocal:ThreadLocal修饰的数据 ,会为每一个线程都准备一份,这样就不会出现问题了。
线程锁
自旋锁
如果一台计算机有一个以上CPU,存在一个美好的假设:很快就会有CPU被释放,因此让需要CPU运行的线程等待,即给这个线程一个自旋锁,Java默认自旋十次。
自适应自旋锁
自旋锁的自旋时间将取决于上一次在同一个锁的自旋时间和锁持有者的状态来决定。如果期望较好则自旋的时间更长。
锁消除
如果一个数据不存在被其它线程访问的可能,则此数据上的锁会经过JVM判断后消除。将不再把此数据视作堆上的数据,而是被视作栈上的数据,认为他们是线程私有的。
锁粗化
扩大锁的范围,如果锁在循环中,那么不停的加锁和解锁会导致算力不必要的消耗。此时可以扩大锁的范围,减少这一部分的损耗。
偏向锁
锁会偏向第一个获得它的线程,如果锁没有被其他线程需要,则不需要进行同步操作,可以提升程序的性能。