赞
踩
进程是系统分配资源和调度的基本单位,也就是说进程可以单独运行一段程序。线程是CPU调度和分派的最小基本单位。
每一个线程都有独一无二的id,不可重复。可以通过Thread.getId()
来获取线程的id。
我们可以在创造现成的时候,给线程起一个名字。这个名字一般用于调试。可以通过Thread.getName()来获取线程名称,通过Thread.setName(String name)来设置线程名称。
线程共有5个状态:新建状态、就绪状态、运行状态、阻塞状态、终止状态。详情可以看下一章节,有5个线程状态的详解。
可以通过Thread.getState()来查看线程的状态。
在Java语言中,每一个线程都有一个优先级,在创建线程的时候就已经设置好了,我们可以自己定义线程的优先级。
默认情况下, 一个线程会继承构造它的那个线程的优先级,即那个线程创建的这个线程,那么这两个线程的优先级相同。
Java中优先级设置了10个级别,MIN_PRIORITY=1,MAX_PRIORITY = 10,NORM_PRIORITY=5
通过调用Thread.setPriority(int priority)方法设置线程优先级,通过Thread.getPriority()方法获取线程优先级。
Java中线程调度器使用优先级调度算法,优先级越大的越先调用。但是线程的执行高度依靠操作系统来进行调度,实际执行时会将Java线程的优先级映射到操作系统上,但是操作系统的优先级并不一定与Java中一一对应,此时会出现优先级不同的线程,被当作优先级相同的情况处理。。
例如Windows系统中仅仅只有7个优先级级别,故我们设置的Java优先级在映射到操作系统中是有可能会产生冲突的。
另外,Oracle为Linux提供的Java虚拟机中,会完全忽略线程优先级——所有线程都有相同的优先级。
综上,目前现在很少使用线程优先级了。
当我们的线程调用start的时候就存活,直至run方法执行完毕,线程自然死亡。或者是一个未捕获的异常终止了run方法而使线程猝死。
可以使用Thread.isAlive()查看线程是否存活,返回值为true为线程存活,反之则不存活。
线程中断就是字面意思,即中断一个正在运行的线程。但是首先我们要明确一点,没有任何语言要求终端的线程必须终止,中断线程只是给系统一个提醒,是否中断要看操作系统自身的调度,以及代码的实现。
举个简单的例子,我们只能作为用户向操作系统发起请求,请求让该系统中断线程,但是我们并没有资格直接去中断线程,最终决策线程是否中断的权利是在操作系统手中的。
可以使用Thread.isInterrupted()查看线程是否被中断,返回值为true为线程终端,反之则未中断。
值得注意的是:不要把interrupt()方法,interrupted()方法与isInterrupted()搞混。
interrupt()方法是普通方法,作用是将此时的标志位设置为true,也就是发送中断请求,改变线程的状态。
interrupted()方法是静态方法,作用是判断当前线程的中断标志位是否设置,该方法有一个副作用,调用后清除标志位,即将线程的中断状态设置为false
isInterrupted()作用则是测试是否被中断,也就是返回标志位的值,该方法不会改变线程的状态,不清除标志位。
守护线程又被称为后台线程,即线程在后台运行。前台线程会阻止进程退出,如果main结束了但是前台线程还没执行完毕,进程不会退出 ,后台线程不阻止进程退出,如果main等的前台线程执行完了,但是后台线程没有执行完,则进程让然会结束。
守护线程不参与线程资源争夺,是独立于其他线程之外的,它会同线程执行一并执行,线程结束它也会结束,常见的日志和GC都属于守护线程。
可以使用Thread.setDaemon()设置成守护线程,设置操作在start前操作,线程启动之后就没法改.
JVM会在一个进程的所有非后台线程结束后,才会结束运行。
JVM中默认有两个线程,main线程和gc线程,gc线程为守护线程,在后台运行。
如果一个进程中只剩下守护线程,那程序的运行也就是没有必要了,守护线程一般是为其他线程服务的。
新建状态(New)
当使用new操作符创建一个新线程时,如new Thread®,这个线程还没有开始运行,这个时候线程的状态就是新建状态(new)。当一个线程处于新建状态时,程序还没有开始运行线程中的代码,在正式线程的运行之前还有一些基础工作要做。
就绪状态(Runnable)
新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程就进入就绪状态(runnable),即所有的准备工作都已经准备好了,就差CPU分配时间片。
由于还没有分配CPU,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。当系统挑选一个等待执行的Thread对象后,它就会从等待状态进入执行状态。系统挑选的动作称之为“CPU调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
运行状态(Running)
该状态是指线程获得了CPU的时间片,线程调用了自己的run方法,线程正在执行中。
阻塞状态(Blocked)
线程在运行途中,一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时,被剥夺了CPU的使用,随即从运行状态(Running)转变为阻塞状态(Blocked)。
在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只能当引起阻塞的原因被消除后,线程转入就绪状态,重新到就绪队列中排队等待,这时被CPU调度选中后会从原来停止的位置开始继续执行。
终止状态(Terminated)
线程调用了stop()、destory()或者run()执行完毕以后,线程随即处于终止状态,又称为死亡状态,处于终止状态的线程不再具有继续运行的能力。
在大多数程序中,可能存在两个或者两个以上的线程需要共享同一对数据,如操作同一个对象,并且每个线程调用了修改该对象的方法。
此时这两个线程的操作会互相覆盖,覆盖的先后顺序取决于线程访问对象的先后顺序,可能会导致对象被破坏。这种情况称为竞态条件。
举个简单的例子,我们在进行银行转账操作的时候,一般需要如下操作:
操作完这三步以后,才能说正确的完成了转账操作。这里我们以操作3进行详细解读:
操作3的具体步骤为:
但是在多线程的情况下,一个线程可能在步骤1和步骤2完毕以后,线程失去CPU进入就绪状态,CPU切换到另外一个线程,另外一个线程执行相同操作,那么在该线程的操作中,仅仅只运行到了步骤1,随即线程2被剥夺运行权,此时切换到线程1,继续执行步骤3,操作完毕以后,线程2开始运行,此时计算的账户金额,是以第一次运行时读取的账户余额为基础计算的,那么重新写回账户的金额就会出错,即将线程1的操作进行覆盖,钱款会存在丢失的情况。
问题就在于一个操作的过程并非原子性,那么解决该问题的方案就是给操作加锁,即防治并发访问代码块。
目前防止并发访问代码块有两种实现方法。Java语言提供了一个synchronized关键字来达到这个目的,Java5以后还引入了一个ReentrantLock类来实现。
这里我们主要讲解ReentrantLock类的使用,在后面讲解synchronized关键字
ReentrantLock类保护代码块不被并发访问的基本结构如下所示:
myLock.lock(); // 获得锁,即锁定操作,防止其他线程闯入
try{
// 需要实现的关键部分
}finally{
// 确保在关键部分执行时,只有一个线程能够进入
mylock.unlock(); // 释放锁,即解锁操作,开放给其他线程
}
这个结构确保任何时刻只有一个线程进入临界区,一旦一个线程锁定了对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们会暂停,直到第一个线程释放这个锁对象。
这里需要重点强调的是必须将解锁操作放到finally子句中进行,否则一旦try语句内抛出一个异常,其他线程将永远阻塞,锁无论在什么情况下,必须释放,才是安全的。
重入锁(reentrantLock):即表示可重新反复进入的锁,但仅限于当前线程,因为一个线程可以反复获得已拥有的锁。
锁拥有一个计数器(hold count)来跟踪对lock方法的嵌套调用,类似于==()、[]、{}==的使用,是成双成对存在的。
线程每调用一次lock方法获得锁后,都必须对应调用一次unlock方法类释放锁。由于这个特性,被一个锁保护的代码可以调用另外一个使用相同锁的方法。
公平锁:是一个倾向于等待时间最长的线程,不过这种锁会严重影响性能。一般情况下,不要求锁是公平的。
即便是使用公平锁,也无法保证线程调度器是公平的。
通常情况下,线程进入临界区后需要进行判断,只有满足去某个条件以后才能够执行。可以使用条件对象来管理那些已经获得锁,但是并不能进行有效工作的线程。
举个简单的例子,我们在进行银行转账操作时,需要判断余额是否充足,如果不充足,则不能进行转账操作,需要进行充值。充值以后,余额充足才能够进行转账操作,但是在判断之前我们就获得了锁,导致其他线程无法进入操作,此时该线程将永无止境的等待下去。
为了解决这种问题,我们需要引入条件对象。
可以使用newCondition方法获取一个条件对象,习惯上会给每个条件对象一个合适的名字来反映他所表示的条件。
从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:
Condition由ReentrantLock对象创建,并且可以同时创建多个,Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁,之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程,使用方式和wait()、notify()类似。
Condition类中的常用方法:
通常await()的调用应当放在条件循环中调用:
while(!(OK to proceed)){
condition.await();
}
当一个线程调用await()方法以后,它没有办法重新自行激活,只能被其他线程使用signal()或者signalAll()激活,如果不存在其他线程调用这两个激活方法,则将陷入死锁。
从经验上讲,只要一个对象的状态有变化,而且有可能对其他线程有利,即就是改变以后符合其他线程的执行要求,则就应当调用signalAll()。
signalAll()并不会立即激活一个等待的线程,只是接触等待线程的阻塞,使这些线程可以在当前线程释放锁后竞争访问对象。
Signal()只是随机选择等待集中的一个线程,并且解除这个线程的阻塞状态。这比接所有线程的阻塞更高效,但是也更危险。如果发生选择的线程已经被解除阻塞,仍不能运行,其会再次阻塞。此时如果没有其他线程调用signal(),那么系统就会陷入死锁。
synchronized关键字会自动提供一个锁,以及相关的条件,对于大多数的需要显示锁的情况,这种机制功能是非常强大的。
通常情况下,我们不需要太精确且复杂的锁控制机制,Java的每个对象内部都维护了一个锁,直接使用synchronized关键字声明方法,则对象的锁将保护整个方法。
当synchronized修饰静态方法时,则会将获得类对象的锁,即对应类.class对象的锁,访问该类时需要使用锁。
使用synchronized修饰的代码段,在该代码段内会获得对象的锁,简单的理解为给代码段加锁,使得代码段的执行具有了原子性。
public class Bank{
private double[] accounts;
private Object lock = new Object();
public void transfer(int from,int to,int cmount){
synchronized (lock){
accounts[from] -= amount;
accounts[to] += amount;
}
}
}
volatile关键字修饰字段,则任何修改该字段的方法都默认被加上锁。
final关键字修饰的变量,一经初始化,则无法修改。
如果修饰的字段是基本数据类型,则不会出现任何问题;但如果修饰的是引用类型的字段,尽管引用不会改变,但是引用指向的对象是会改变的,如果多线程对该对象实体进行操作,仍然需要同步。
java.util.curcurrent.atomic包下的很多类使用了很高效的机器级别指令来保证操作的原子性。
例如:AtomicInteger类提供的incrementAndGet()和decrementAndget()方法实现了原子性的自增和自减。
// 定义一个数
int num = 1;
// 使其自增
num ++; // 这个操作并不是原子性的,再多线程的操作时会出现问题。
System.out.println(num) // 此时num == 2
// 使用AtomicInteger
public static AtomicInteger atomic = new AtomicInteger();
num = 1; // 重新对num赋值
num.incrementAndGet(); // 调用自增方法
System.out.println(num) // 此时num == 2
该包下的类还有很多方法提供,具体的使用可以查阅文档。
两个线程互相持有对方所需要的锁,但只有在获得锁,执行完以后才能释放锁,此时进入僵局,故为死锁。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
生产者线程向队列插入元素,消费者线程向队列输出元素。使用队列时可以安全的从一个线程向另一个线程传递数据。
当试图向队列添加元素而队列已满,或者是从队列移出元素而队列为空时,阻塞队列将导致线程阻塞。
put(E e):将元素添加到队列的尾部,如果队列已满,则阻塞等待直到队列有空闲空间。
take():从队列头部移除并返回元素,如果队列为空,则阻塞等待直到队列有新元素。
offer(E e):将元素添加到队列的尾部,如果队列已满,则立即返回false。
poll():从队列头部移除并返回元素,如果队列为空,则立即返回null。
offer(E e, long timeout, TimeUnit unit):将元素添加到队列的尾部,如果队列已满,则阻塞等待指定的时间,超时后返回false。
poll(long timeout, TimeUnit unit):从队列头部移除并返回元素,如果队列为空,则阻塞等待指定的时间,超时后返回null。
remainingCapacity():返回队列中剩余的可用空间。
size():返回队列中的元素个数。
java.util.concurrent.ArraysBlockingQueue:构造一个有指定容量和公平性设置的阻塞队列。队列实现为一个循环数组。
java.util.concurrent.LinkedBlockingQueue:构造一个无上限的阻塞队列或者双向队列,实现为一个链表。
java.util.concurrent.LinkedBlockingDeque:根据容量构建一个有限的阻塞队列或者双向队列,实现为一个链表。
java.util.concurrent.DelayQueue:构造一个包含Delay元素的无上限阻塞队列,只有那些延迟已经到期的元素可以从队列中移除。
这些类的size()方法不一定在常量时间内完成操作。
任何集合类使用同步包装器变成线程安全的。
List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>());
Map<K,V> synchHashMap = Collections.synchronizedMap(new HashMap<K,V>());
示例将ArrayList和HashMap都变成了线程安全的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。