线程的优点: 1.提供资源的利用率 2.提高程序性能
线程的风险: 1. 死锁 2. 上下文切换 3. 线程安全
多线程的实现方式
Thread类
Runnable接口
Callable
future
当多个线程同一时间运行同一段代码,线程会竞争,成员变量就会产生问题。那么如何解决这类问题呢?
给方法加 synchronized(同步锁),建议给关键代码加 synchronized 代码块 这样效率会比加在方法上效率快。 synchronized是个内部锁:对象内部给我们提供的锁,每个对象都会有个状态变量,每个线程去访问时,都要去判断它的状态变量。同时它也是个可重入锁,意思是:当一个线程去获取它已经持有的锁,它是能够成功的。锁是线程间互斥的,但是同一个线程是不会互斥的。synchronized能够解决可见性,原子性。Atomic原子性 当给成员变量加了原子性后,也相当于给成员变量加上了锁。但这个实现跟 synchronized 不一样,因为原子性并不会排队执行。指令重排序:编译器和处理器为了提高程序的运行性能,对指令进行重新排序。但是如果某条指令是依据前面的指令而执行的,那么这条指令肯定是在最后执行的。
重排序分三种类型:
编译器优化的重排序 编译器在不改变单线程程序语义的前提下(代码中不包含synchronized关键字),可以重新安排语句的执行顺序。
指令级并行的重排序 现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序。 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
volatile(volatile只能修饰成员变量或者成员类,如果修饰了成员类,该类的所有变量都会有可见性。它不能解决线程安全问题) :
保证变量的修改让所有线程可见阻止指令重排。(这个阻止指令排序是相对的,意思是,它只会阻止被它修饰的成员变量排序。用synchronized加锁或者lock加锁也可以阻止指令重排)一般用在关键的变量转换的位置,例如通过某个成员变量来阻止程序的运行。volatile是一个比较古老的关键字,synchronized现在优化的比该关键字好很多了,所以一般不建议使用它。一般使用的情况是在:想要阻止指令重排或者是让某个成员状态码让线程可见。volatile只能解决可见性。
先比较,后操作称为CAS操作 compare and set 一般并发都是由该原因形成的,需要将其变成原子性就一般能解决。(多线程编程必问)什么时候会出现线程并发问题:多线程操作同一个变量时,不光是写读的时候也会出现。如何解决呢?
将变量修饰成final不要共享变量线程封闭 如何达到线程封闭: 1 、栈封闭:把变量的修改全部放在一个方法里面。比如说方法内部声明,这样就不会溢出了。 2 、 ThreadLocal 线程绑定 它会给当前线程绑定一套数据(在管理链接方面经常使用)同步容器:同步容器就是给所有方法加上了synchronized。缺点:效率低 可能会线程不安全 迭代过程中发生修改,这时会报并发修改异常(声明锁,一般都用私有锁 private) 相关容器:
vectorHashtable(双列线程安全集合)Collections.synchronizedList并发容器: 相关容器:
ConcurrentHashMap:采用了分段锁。分段锁:每个元素(链表或者是桶)锁起来,就可以N个线程同时操作。CopyOnWriteArrayList/Set : 修改线程会独立拷贝一份数据去操作。 一般在读操作使用,写操作少。BlockingQueue:阻塞队列(当队列的容量满了就会阻塞在原地不动,只有当有空间了,才会解除阻塞状态)。put往队列头塞东西,take往队列尾拿东西。闭锁:当一个线程依赖另外一个线程的完成才能进行继续操作。那我们怎么知道另外一个线程完成了呢?这个时候就要用到闭锁。CountDownLatch(闭锁api)
栅栏:所有线程必须达到某一个状态时,这些线程才能继续操作。相当于水坝。使用场景:数据库写入,当所有线程都写入完成时,才能继续后面的操作。CyclicBarrier(栅栏api)
信号量:当资源数目和线程不对等的情况下,使用信号量。使用场景:当线程数量>资源数目时,某个线程释放了一个资源,另外一个线程就顶上。Semaphore(信号量api)
Executor线程池是个接口,因为不能很好的关闭线程池,所以衍生了ExecutorService(工具类),是Executor的子类,是Executor的拓展类,里面提供了很多Executor没有的方法。
相关线程池: ScheduledThreadPoolExecutor(任务调度线程池)ThreadPoolExecutor
死锁:多个并发进程因争夺系统资源而产生相互等待的现象。
什么情况下会出现死锁:1.锁的嵌套 2.锁的嵌套顺序是乱序的 怎么避免死锁的情况: 1.尽量不要锁的嵌套 2.锁的嵌套顺序规定好 2.启用超时放弃锁的机制
引入显式锁的超时机制特性来避免死锁 lock:显式锁Lock,Lock是一个接口,定义了一些抽象的所操作。与内部锁机制不同,Lock提供了无条件,可轮询,定时的,可中断的锁获取操作,所有加锁和解锁的方法都是显式的。
读-写锁(ReadWriteLock):ReadWriteLock,暴露了2个Lock对象,一个用来读,另一个用来写。读取ReadWriteLock锁守护的数据,你必须首先获得读取的锁,当需要修改ReadWriteLock守护的数据,你必须首先获得写入锁。相比Thread线程间是不用排队来读操作的。这样效率明显很高。但是ReadWriteLock只允许一个写者。 使用场景:读操作比较多可以使用这个锁。
公平锁:先来的先读,是排队来的锁,效率比较低。在公平锁中,选择权交给等待时间最长的线程;如果锁由读者获得,而一个线程请求写入锁,那么不再允许读者获得读取锁,直到写者被受理,平且已经释放了写锁。
非公平锁:上一个线程释放了锁,接下来的使用权谁先抢到,就谁使用,效率高。在非公平的锁中,线程允许访问的顺序是不定的。由写者降级为读者是允许的;从读者升级为写者是不允许的(尝试这样的行为会导致死锁)
他对世界比较乐观,认为别人访问正在改变的数据的概率是很低的,所以直到修改完成准备提交所做的的修改到数据库的时候才会将数据锁住。完成更改后释放。
我想一下一个这样的业务场景:我们从数据库中获取了一条数据,我们正要修改他的数据时,刚好另外一个用户此时已经修改过了这条数据,这是我们是不知道别人修改过这条数据的。
解决办法,我们可以在表中增加一个version字段,让这个version自增或者自减,或者用一个时间戳字段,这个时间搓字段是唯一的。我们写数据的时候带上version,也就是每个人更新的时候都会判断当前的版本号是否跟我查询出来得到的版本号是否一致,不一致就更新失败,一致就更新这条记录并更改版本号。
1.查询出商品信息 select (status,status,version) from t_goods where id=#{id} 2.根据商品信息生成订单 3.修改商品status为2 update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};用户体验表现层面通常表现为系统繁忙之类的。 在这里还要注意乐观锁的一个细节:就是version字段要自增或者自减,否者会出现ABA问题。
ABA问题:线程Thread1拿到了version字段为A,由于CAS操作(即先进行比较然后设值),线程Thread2先拿到的version,将version改成B,线程Thread3来拿到version,将version值又改回了A。此时Thread1的CAS(先比较后set值)操作结束了,继续执行,它发现version的值还是A,以为没有发生变化,所以就继续执行了。这个过程中,version从A变为B,再由B变为A就被形象地称为ABA问题了。
也称排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防止其他进程读取或修改表中的数据。
一般使用 select …for update 对所选择的数据进行加锁处理,例如
select * from account where name=”JAVA” for update,这条sql 语句锁定了account 表中所有符合检索条件(name=”JAVA”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
用户界面常表现为转圈圈等待。
如果数据库分库分表了,不再是单个数据库了,那么我们可以用分布式锁,比如redis的setnx特性,zookeeper的节点唯一性和顺序性特性来做分布式锁。