JAVA面试汇总(二)多线程(六)
JAVA多线程内容比较多,今天写完了第六篇,后边还有七(肯定最后一篇了)。
- 如何保证多线程读写文件的安全?
(1)读写互斥,写读互斥,写写互斥,只有读读相容(可以异步)
(2)FileInputStream、FileOutputStream、RandomAccessFile均可得到FileChannel对象
(3)FileChannel通过独占锁tryLock()锁定文件,用于写文件。
(4)FileChannel的tryLock(0, Long.MAX_VALUE, true)是非阻塞的,是共享锁,能被多个线程同时持有,它能禁止其他线程获取独占锁(防止写进程进来写文件),可用于读文件。
(5)FileChannel的lock()是阻塞的,在文件被锁定的情况下,会保持阻塞,直到获得该锁为止,实际上这个可以用作喝tryLock一样的情况,用于写文件。
//一般用这种方式,来循环获取锁,获取到锁之后开始写操作。
File file;
try {
file = FileUtils.createFile(pathFile);
} catch (IOException e) {
e.printStackTrace();
ioListener.onFail("文件创建失败,请检查路径是否合法以及读写权限");
return;
}
FileOutputStream fileOutputStream = new FileOutputStream(file);
FileChannel fileChannel = fileOutputStream.getChannel();
//文件锁
FileLock fileLock = null;
while (true) {
try {
fileLock = fileChannel.tryLock(0, Long.MAX_VALUE, true);//共享锁
break;
} catch (Exception e) {
System.out.println("有其他线程正在操作该文件,当前线程" + Thread.currentThread().getName());
}
}
- Java中的ReadWriteLock是什么?
(1)ReetrantReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。
(2)读锁使用共享模式;写锁使用独占模式;读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockTest {
//默认是非公平锁
// static ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
//手工设置为公平锁
static ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock(true);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// new Thread(new RunnableDowngradeLock()).start();
new Thread(new RunnableUpgradeLock()).start();
}
}
//ReentrantReadWriteLock支持锁降级,但是一定记得显示的把两个锁都释放
static class RunnableUpgradeLock implements Runnable{
@Override
public void run() {
try {
//先获取写锁
rtLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + " get writeLock");
//再获取读锁,这种明显是降级,锁降级
rtLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + " get read lock");
System.out.println(Thread.currentThread().getName() + " isWriteLocked "+ rtLock.isWriteLocked());
System.out.println(Thread.currentThread().getName() + " isFair "+ rtLock.isFair());
}finally {
rtLock.writeLock().unlock();
rtLock.readLock().unlock();
}
}
}
//ReentrantReadWriteLock不支持锁升级,下面这个执行会死锁
static class RunnableDowngradeLock implements Runnable{
@Override
public void run() {
try {
//先获取读锁
rtLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + " get read lock");
//再获取写锁,这种明显是升级了,锁升级
rtLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + " get writeLock");
}finally {
rtLock.writeLock().unlock();
rtLock.readLock().unlock();
}
}
}
}
- lock原理
(1)Lock接口就这么几个方法
//尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock()
//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly()
//尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit)
//释放锁
void unlock()
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition()
(2)一定有一个变量是表示(锁)状态的变量(假设0表示没有线程获取锁,1表示已有线程占有锁),该变量必须声明为voaltile类型(任何线程过来,都要直接从内存读取锁锁状态,不可能从缓存取);
(3)锁操作步骤:1>先获取锁状态,如果状态时0未锁定时候,修改锁状态为1,返回锁定成功;2>如果时1已锁定,进入等待队列,将自身阻塞,等待锁变为0未锁定的唤醒该线程,然后执行第一步;3>修改锁状态时,有可能失败,抢占式的,只有第一个修改的能成功,后边的线程修改都失败,同样进入等待队列,将自身阻塞,等待锁变为0时唤醒该线程,然后执行第一步
(4)解除锁操作步骤:1>修改锁状态,将1变为0,然后通知等待队列的第一个线程(如果是公平锁的话,第一个等待的),释放完成;2>被唤醒的锁,执行上面的所操作步骤。3>如果锁是已经被释放了,还来解除锁操作,这个时候直接抛异常失败。
(5)公平锁实现:锁操作时,判断队列是否存在等待的线程,1>如果有,就把自己加入队列,然后将自身阻塞;2>如果没有等待的线程,执行所操作步骤第一步。3>队列按照先入先出操作,所以唤醒的一定是先进入队列的线程
(6)非公平锁实现:实际上就是是否可以插入到队列前面,第一个被唤醒一定是队列头的等待线程,哪个线程抢占了队列头,谁就是第一个。
(7)Condition接口,实际上就是维护的等待队列,先进先出队列
-
ReentrantLock的内部实现
(1)其实上面Lock已经说的差不多了
(2)内部有抽象类Sync继承了AbstractQueuedSynchronizer。Sync有两个子类,一个FairSync另一个NonfairSync(ReentrantLock默认就是非公平锁)。
(3)这个比Lock多了acquire和tryAcquire的方法,还有tryRelease/getOwner/getHoldCount/isLocked等方法。
(4)tryAcquire方法分为公平锁/非公平锁的,公平锁:如果锁状态为0且等待队列没有等待的线程,则通过cas方式设置锁状态为1,设置拥有锁的线程为自己;如果锁状态大于1,判断拥有锁的是不是自己,是自己则将状态改为加1,返回成功,如果拥有锁的不是自己,返回失败。非公平锁:如果锁状态为0,就通过cas方式抢占锁,抢占成功设置状态,设置拥有锁的线程为自己,如果锁状态不为0,在判断拥有锁的是不是自己,是则+1,返回成功,不是则直接返回抢占失败。
(5)acquire方法实际上在AbstractQueuedSynchronizer(AQS)里,这个会调用(4)的tryAcquire方法,如果失败把自己加入到等待队列中,成功则直接返回成功。
(6)tryRelease就是tryAcquire的反向,为锁状态减去准备release的个数,也就是释放锁的个数。但是这个要判断解锁的是否为当前拥有锁的线程,否则直接失败。 -
多线程有什么要注意的问题?
(1)需要创建大量多线程的程序,建议使用线程池,避免占用大量资源
(2)控制并发数量,过大可能导致服务器无法支撑,cpu就那么多,大量并发执行反而卡死了
(3)通过一些锁来控制并发导致的共享问题
(4)使用锁的过程中注意开发时候尽量别设计成循环等待的,容易死锁
(5)另外ReentrantLock一类的锁,lock后,一定要记得unlock,否则就死锁了
(6)有些线程任务执行完成一部分后,需要等待其他线程执行完成后再执行,这个时候可以通过sleep或者其他方式使其进入阻塞状态。
(7)有没有优先级?