首先说明:本文默认对java的线程有一定的基础。
一个线程的创建有2种办法,继承Thread类和实现Runnable接口,具体的操作在这儿就不在赘述了,但是在我们看了jdk的api后就会发现,其实Thread它也实现了Runnable接口的,可能大家就要问了,既然是这样,那只有Runnable接口不就对了吗?Thread类还有存在的必要吗,答案是肯定的,因为接口不允许new,接口中没有构造函数。那问题又来了,我什么时候用Thread什么时候用Runnable呢?jdk的官方文档中是这样说的“Runnable接口应该由那些打算通过某一线程执行其实例的类来实现”(看的懂啥意思,但还是不知道怎么用,汗!!!)。我得理解是如果只想重写 run() 方法,而不重写其他 Thread 方法,那么应使用 Runnable 接口,该类已经继承了一个其他的类时使用Runnable接口。
1.先让我们来看一下java的同步(synchronized)和锁(lock)机制
同步代码块:
synchronized (object) {
//doSomething
}
Java多线程支持方法同步,方法同步只需用用synchronized来修饰方法即可,那么这个方法就是同步方法了
对于同步方法而言,无需显示指定同步监视器,同步方法监视器就是本身this
同步方法:
public synchronized void editByThread() {
//doSomething
}
注意:synchronized可以修饰方法、代码块,但不能修饰属性、构造方法
2)同步锁(Lock)
通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock更灵活的结构,有很大的差别,并且可以支持多个Condition对象
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,
线程开始访问共享资源之前应先获得Lock对象。不过某些锁支持共享资源的并发访问,如:ReadWriteLock(读写锁),在线程安全控制中,
通常使用ReentrantLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁。
class C {
//锁对象
private final ReentrantLock lock = new ReentrantLock();
......
//保证线程安全方法
public void method() {
//上锁
lock.lock();
try {
//保证线程安全操作代码
} catch() {
} finally {
lock.unlock();//释放锁
}
}
}
1)把一段代码声明成synchronized后这段代码就有原子性和可见性,所谓的原子性就是这段代码在同一时刻只能被一个线程所访问。可见性是指这段代码的数据的一致性即下个线程访问的时候的数据应该是上个线程最后处理的数据,保证线程看到的共享变量的值前后一致。
synchronized 和lock的区别:
2.ReentrantLock必须在finally中释放锁,否则后果很严重,编码角度来说使用synchronized更加简单,不容易遗漏或者出错。
3.ReentrantLock 的性能比synchronized会好点。
4.ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。
下面通过一些java多线程的面试题来更好的理解。
1.Java中notify 和 notifyAll有什么区别?
答:这又是一个刁钻的问题,因为多线程可以等待单监控锁,Java API 的设计人员提供了一些方法当等待条件改变的时候通知它们,但是这些方法没有完全实现。notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
2.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
答:这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。因为所有的类都继承了Object类,这样所有的类都可以进行多线程编程了
3.为什么wait和notify方法要在同步块中调用?
答:主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。
4.有三个线程T1,T2,T3,怎么确保它们按顺序执行?
答:在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
5.在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
答:lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。
答:最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
7.什么是死锁,如何分析和避免死锁?
答:死锁就是两个或两个以上的线程被无限的阻塞,线程之间相互等待所需资源。这种情况可能发生在当两个线程尝试获取其它资源的锁,而每个线程又陷入无限等待其它资源锁的释放,除非一个用户进程被终止。就JavaAPI而言,线程死锁可能发生在一下情况。
当两个线程相互调用Thread.join(),当两个线程使用嵌套的同步块,一个线程占用了另外一个线程必需的锁,互相等待时被阻塞就有可能出现死锁。避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。