并发
在一个单独的线程里执行一个任务的简单过程
// 1.将任务代码移到实现了Runnable接口的类的run方法中。
public interface Runnable() {
void run();
}
// 由于Runnable是一个函数式接口,可以使用lambda表达式。
Runnable r = () -> { /* task code */ };
// 2.由Runnable创建一个Thread对象
Thread t = new Thread(r);
// 3.启动线程
t.start();
也可以通过构建一个Thread类的子类定义一个线程,然后,构造一个子类的对象,并调用start方法。不过,这种方法已不再推荐(但是我们一直在使用)。应该将要并行运行的任务与运行机制解耦。如果有很多任务,要为每个任务创建一个独立的线程所付出的代价太大了。
不要调用Thread类或Runnable对象的run方法。直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。应该调用Thread.start方法。这个方法将创建一个执行run方法的新线程。
线程状态
线程可以有如下6种状态
-
New(新创建)
-
Runnable(可运行)
-
Blocked(被阻塞)
-
Waiting(等待)
-
Timed waiting(计时等待)
-
Terminated(被终止)
要确定一个线程的当前状态,可调用getState方法。
线程属性
线程优先级
默认情况下,一个线程的优先级继承它父线程的优先级。
可以用setPriority方法提高或降低任何一个线程的优先级。
可以将优先级设置为1-10
MIN_PRIORITY(在Thread类中定义为1)
MAX_PRIORITY(定义为10)。
NORM_PRIORITY被定义为5。
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。
守护线程
t.setDaemon(ture);
守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
同步
锁对象
synchronized关键字
Java SE 5.0引入了ReentrantLock类
private Lock myLock = new ReentrantLock();
myLock.lock();
try {
// do something
}
finally {
myLock.unlock();
}
确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
把解锁操作括在finally子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
// java.util.concurrent.locks.Lock:
void lock();
void unLock();
// java.util.concurrent.locks.ReentrantLock:
ReentrantLock(); // 构建一个可以被用来保护临界区的可重入锁。
ReentrantLock(boolean fair); // 构建一个带有公平策略的锁。
一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能。所以,默认情况下,锁没有被强制为公平的。听起来公平锁更合理一些,但是使用公平锁比使用常规锁要慢很多。
条件对象
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程
Java库中条件对象的实现。Condition
// java.util.concurrent.locks.Lock
Condition newCondition(); //返回一个与该锁相关的条件对象。
// java.util.concurrent.locks.Condition
void await(); // 将该线程放到条件的等待集中。
void signalAll(); // 解除该条件的等待集中的所有线程的阻塞状态。
void signal(); // 从该条件的等待集中随机地选择一个线程,解除其阻塞状态。
有关锁和条件的关键之处:
-
锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
-
锁可以管理试图进入被保护代码段的线程。
-
锁可以拥有一个或多个相关的条件对象。
-
每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
Lock和Condition接口为程序设计人员提供了高度的锁定控制。
synchronized
如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
public synchronized void method() {
// method body
}
// 等价于
public void method() {
this.intrinsicLock.lock();
try {
// method body
} finally {
this.intrinsicLock.unlock();
}
}
可以简单地声明方法为synchronized,而不是使用一个显式的锁。
使用synchronized关键字来编写代码要简洁得多。当然,要理解这一代码,你必须了解每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。
静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。
在代码中应该使用哪一种?Lock和Condition对象还是同步方法?下面是一些建议:
-
最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下你可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。
-
如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。
-
如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition。
同步阻塞
private Object lock = new Object();
public void method() {
sychronized(lock) {
// code
}
}
在此,lock对象被创建仅仅是用来使用每个Java对象持有的锁。(信号量)