面试(六) 多线程

一、多线程的实现

1.实现runnable接口

public class ThreadTest {
	public static void main(String[] str){
		MyThread myThread = new MyThread();//只需一个对象即可
		new Thread(myThread, "线程1").start();
		new Thread(myThread, "线程2").start();
	}
}
class MyThread implements Runnable{
	public void run() {
		for(int i = 0; i<10; i++){
			System.out.println(i);
		}
	}
}

2.继承Tread类

public class ThreadTest {
	public static void main(String[] str){
		new MyThread("线程1").start();
		new MyThread("线程2").start();
		new MyThread("线程3").start();
for(int i = 0; i<5; i++){
			System.out.println(9999);//先打印9999
		}
	}
}
class MyThread extends Thread{
	public MyThread(String name) {
		super(name);//调用父类的构造方法
	}
	public void run() {
		for(int i = 0; i<10; i++){
			System.out.println(i);
		}
	}
}


二、锁

共享锁(读锁,S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁,直到已释放所有共享锁。获准共享锁的事务只能读数据,不能修改数据。

互斥锁(写锁,排它锁、独占锁、X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的锁,直到在事务的末尾将资源上的锁释放为止。获准排他锁的事务既能读数据,又能修改数据。

自旋锁: 当进程进入CPU运行时,就会给它的代码上锁,以免别的CPU中的进程修改里面的代码。所谓子旋锁就是这样的一把锁:进程A进入CPU,锁上门运行,进程B来到CPU前,发现门被锁上了,于是等待进程A出来交出开锁钥匙。


1.互斥锁

    互斥是进程同步关系的一种特殊情况,相当于只存在一个临界资源。

    互斥是保守的加锁策略,避免了“写//的重叠,但是同样避开了“读/的重叠。只要每个线程保证能够读到新的数据,并且在读线程读取数据的时候没有其他线程修改数据,就不会发生问题。读-写锁允许的情况是:一个资源能够被多个读线程访问,或者被一个写线程访问,但两者不能同时进行。

    对于所有的互斥锁,线程都是从sleep(过程中会放弃cpu)加锁到running解锁,过程中有上下文的切换,cpu的抢占,信号的发送等开销。


synchronized(内部锁) 非公平锁

    特点:

    使用synchronized将需要互斥的代码包含起来,并上一把锁。

    一旦死锁必须重启程序。(无法中断一个正在等候获得锁的线程)

    一旦发起锁请求,该请求就不能停止了,如果不能获得锁,则当前线程会阻塞并等待获得锁。

    synchronized将++i,i++操作变成一个原子操作

    使用方法:

    1) synchronized {普通方法}:同一时间只能有一个线程访问同一个对象的该方法。缺点:同步整个方法效率不高。

    2) synchronized {代码块}:对代码块执行线程同步,效率要高于对整个函数执行同步,推荐使用这种方法。

    3) synchronized {static方法}:加锁的对象是类,同一时间,该类的所有对象(多个对象)中的synchronizedstatic方法只能 有一个线程访问。

    4) synchronized {run方法}:此时为同步普通方法的特殊情况,由于在线程的整个生命期内run方法一直在运行,因此同一个Runnable对象的多个线程只能串行运行。 

    ①当多个并发线程访问同一个对象的同步代码块时,一段时间内只能有一个线程得到执行,其他线程必须等待当前线程执行完代码块后再执行代码;

    ②当一个线程访问一个对象的同步代码块时,其他线程可以访问该对象的中的非同步代码块;

    ③当一个线程访问一个对象的同步代码块时,其他线程对该对象中的所有同步代码块均不能访问;


    通信方式:

    Object.wait():线程调用此方法后,主动释放对象锁,同时本线程进入对象等待池中处于阻塞状态。只有当其他线程调用同个对象的notify()或notifyAll()方法后,才可能激活为就绪状态。

    Object.notify():JVM会根据调度策略调取一个对象等待池中的线程,将其从阻塞状态激活为就绪状态,当此线程再次获得对象锁和CPU后,就可以进入执行状态。

    notify()是根据调度策略激活某一个线程,notifyAll()是将所有处于等待线程池中的线程全部激活为就绪状态,但是激活后就绪状态的线程要想重新执行,必须再次获得对象锁。

    sleep()、yield()这两个方法都会让当前正在执行的线程处于暂时停止执行的状态,交出CPU的使用权一段时间, 在暂停线程的同时不会释放已获得的对象锁:

    1) Thread.sleep方法必须带一个时间参数,单位毫秒,当线程执行sleep后,在指定时间内,将转为阻塞状态;Thread.yield方法不带参数,当线程执行yield后,线程将进入就绪状态。

    2)Thread.sleep会抛出InterruptedException异常,而Thread.yield方法不会抛出异常。

    yield()方法检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。yield()只是提前结束当前线程占用CPU的时间,线程转为就绪状态,等待下一个时间片再继续获得CPU并执行。

    Thread.join()可以将多线程的异步变为同步,在父线程调用子线程的join方法后,必须等待子线程执行结束,父线程才会继续执行下去。Thread.join()方法会抛出InterruptedException异常。

    volatile是synchronized的一种弱实现,它可以保证变量的可见性,而不能保证程序执行的原子性。JVM只能保证从主内存加载到线程工作栈中的值是最新的,但使用过程不能完全保证线程对该变量同步的情况。

    synchronized总结在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断。与synchronized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的,即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout,TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。你也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock方法。


ReentrantLock可重入锁

    ReentrantLock通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。(即要把互斥区放在try内,释放锁放在finally内)


    ReentantLock(在高并发量情况下使用)继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

    1.可响应中断锁ReentrantLock的在获取锁的过程中有2种锁机制,忽略中断锁lock.lock()和响应中断锁lock.lockInterruptibly()。当等待线程A或其他线程尝试中断线程A时,忽略中断锁机制则不会接收中断,而是继续处于等待状态;响应中断锁则会处理这个中断请求,并将线程A由阻塞状态唤醒为就绪状态,不再请求和等待资源。

    2.可轮询锁请求在synchronized中,一旦发生死锁,唯一能够恢复的办法只能重新启动程序.tryLock()轮询方法来获得锁,如果锁可用则获取锁,如果锁不可用,则此方法返回false,并不会为了等待锁而阻塞线程,这极大地降低了死锁情况的发生。

    3.定时锁在synchronized中,一旦发起锁请求,该请求就不能停止了,如果不能获得锁,则当前线程会阻塞并等待获得锁。lock.tryLock(long timeout, TimeUnit unit)来指定让线程在timeout单位时间内去争取锁资源,如果超过这个时间仍然不能获得锁,则放弃锁请求,定时锁可以避免线程陷入死锁的境地。

 

    通信方式 Condition  await(),signal(),signalAll()

    锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用new Condition()方法, 创建多个Condition,在不同的情况下使用不同Condition即可。

2.Semaphore信号量

    Semaphore信号量来完成多个临界资源的访问。

    通过acquire()release()方法来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。

    此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquiretryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。

    Semaphore的锁释放操作也由手动进行,因此与ReentrantLock一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成。

    Semaphore支持多个临界资源(各线程采取互斥的方式,实现共享的资源称作临界资源。),而ReentrantLock只支持一个临界资源,可以认为ReentrantLockSemaphore的一种特殊情况。Semaphore的使用方法与ReentrantLock非常相似。


3.AtomicInteger等一些同步类


4.读写锁

    ReadWriteLock接口维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader线程同时保持。写入锁是独占的。

    实现类ReentrantReadWriteLock中定义了2个内部类, ReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock。


5.互斥锁与自旋锁区别

    互斥锁,线程都是从sleep(过程中会放弃cpu)加锁到running解锁,过程中有上下文的切换,cpu的抢占,信号的发送等开销。

自旋锁,线程一直是running(加锁——>解锁),不放弃cpu

互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。

6.公平锁和非公平锁的区别

在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁(可用ReentrantLock)上,则允许插队(抢占):当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值