一, 多线程出现的安全问题:
1, 问题的原因:
多个线程执行的不确定性引起执行结果的不稳定. 当多条语句在操作同一个线程共享数据时,
一个线程对多条语句只执行了一部分, 还没有执行完, 另一个线程参与进来执行, 导致共享数据的错误.
2, 解决的办法:
对多条操作共享数据的语句, 只能让一个线程都执行完, 在执行过程中, 其他线程不可以参与执行.
二, Synchronized 的使用方法:
1, 操作共享数据的代码, 即为需要被同步的代码. 不能包含代码多或者少.
2, 共享数据: 多个线程共同操作的变量. 比如: ticket 就是共享数据.
3, 同步监视器, 俗称: 锁. 任何一个类的对象, 都可以充当锁.
4, 要求: 多个线程必须要共用同一把锁.
三, 同步机制中的锁:
1, 同步锁机制:
对于并发工作, 你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争).
防止这种冲突的方法就是当资源被一个任务使用时, 在其上加锁.
第一个访问某项资源的任务必须锁定这项资源, 使其他任务在其被解锁之前,
就无法访问它了, 而在其被解锁之时, 另一个任务就可以锁定并使用它了.
2,synchronized 的锁是什么:
任意对象都可以作为同步锁. 所有对象都自动含有单一的锁(监视器).
同步方法的锁: 静态方法(类名. class) , 非静态方法(this)
同步代码块: 自己指定, 很多时候也是指定为 this 或类名. class
3, 注意事项:
必须确保使用同一个资源的多个线程共用一把锁,
这个非常重要, 否则就无法保证共享资源的安全.
一个线程类中的所有静态方法共用同一把锁(类名. class) ,
所有非静态方法共用同一把锁(this) , 同步代码块( 指定需谨慎)
例子: 创建三个窗口卖票, 总票数为 100 张. 使用实现 Runnable 接口的方式
1, 问题: 卖票过程中, 出现了重票, 错票 -->出现了线程的安全问题.
2, 问题出现的原因: 当某个线程操作车票的过程中, 尚未操作完成时, 其他线程参与进来, 也操作车票.
3, 如何解决: 当一个线程 a 在操作 ticket 的时候, 其他线程不能参与进来. 直到线程 a 操作完 ticket 时,
其他线程才可以开始操作 ticket. 这种情况即使线程 a 出现了阻塞, 也不能被改变.
4, 在 Java 中, 我们通过同步机制, 来解决线程的安全问题.
- class Windows implements Runnable{
- private int ticket = 100;
- @Override
- public void run() {
- while(true){
- synchronized (this){
- // 此时的 this: 唯一的 Window1 的对象
- if (ticket> 0) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()
- + ": 卖票, 票号为:" + ticket);
- ticket--;
- } else {
- break;
- }
- }
- }
- }
- }
- public class WindowTest {
- public static void main(String[] args) {
- Window1 w = new Window1();
- Thread t1 = new Thread(w);
- Thread t2 = new Thread(w);
- Thread t3 = new Thread(w);
- t1.setName("窗口 1");
- t2.setName("窗口 2");
- t3.setName("窗口 3");
- t1.start();
- t2.start();
- t3.start();
- }
- }
使用同步方法解决实现 Runnable 接口的线程安全问题.
关于同步方法的总结:
1, 同步方法仍然涉及到同步监视器, 只是不需要我们显式的声明.
2, 非静态的同步方法, 同步监视器是: this.
3, 静态的同步方法, 同步监视器是: 当前类本身.
- class Windows implements Runnable {
- private int ticket = 100;
- @Override
- public void run() {
- while (true) {
- show();
- }
- }
- private synchronized void show(){
- // 同步监视器: this
- //synchronized (this){
- if (ticket> 0) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()
- + ": 卖票, 票号为:" + ticket);
- ticket--;
- }
- }
- }
- public class WindowTest {
- public static void main(String[] args) {
- Window3 w = new Window3();
- Thread t1 = new Thread(w);
- Thread t2 = new Thread(w);
- Thread t3 = new Thread(w);
- t1.setName("窗口 1");
- t2.setName("窗口 2");
- t3.setName("窗口 3");
- t1.start();
- t2.start();
- t3.start();
- }
- }
四, 线程同步的范围:
1, 如何找问题, 即代码是否存在线程安全 (非常重要 )
(1)明确哪些代码是多线程运行的代码.
(2)明确多个线程是否有共享数据.
(3)明确多线程运行代码中是否有多条语句操作共享数据.
2, 如何解决呢 (非常重要)
对多条操作共享数据的语句, 只能让一个线程都执行完,
在执行过程中, 其他线程不可以参与执行.
即所有操作共享数据的这些语句都要放在同步范围中.
3, 切记:
范围太小: 没锁住所有有安全问题的代码
范围太大: 没发挥多线程的功能.
4, 释放锁的操作:
●当前线程的同步方法, 同步代码块执行结束.
●当前线程在同步代码块, 同步方法中遇到 break,returm 终 止了该代码块, 该方法的继续执行.
●当前线程在同步代码块, 同步方法中出现了未处理的 Error 或 Exception, 导致异常结束.
●当前线程在同步代码块, 同步方法中执行了线程对象的 wait()方法, 当前线程暂停, 并释放锁.
5, 不会释放锁的操作:
●线程执行同步代码块或同步方法时, 程序调用 Thread. sleep(),Thread.yield()方法暂停当前线程的执行.
●线程执行同步代码块时, 其他线程调用了该线程的 suspend()方法将该线程挂起,
该线程不会释放锁 (同步监视器), 应尽量避免使用 suspend() 和 resume()来控制线程.
6, 线程的死锁问题:
●死锁原因:
不同的线程分别占用对方需要的同步资源不放弃, 都在等待对方放弃自己需要的同步资源,
就形成线程死锁. 出现死锁后, 不会出现异常或提示, 只是所有的线程都处于阻塞状态, 无法继续.
●解决方法:
专门的算法或原则, 尽量减少同步资源的定义, 尽量避免嵌套同步.
五, Lock(锁):
1, 从 JDK 5.0 开始, Java 提供 了更强大的线程同步机制一通过显式定 义同
步锁对象来实现同步. 同步锁使用 Lock 对象充当.
2,java.util.concurrent locks.Lock 接口是控制多个线程对共享资源进行访问的
工具. 锁提供了对共享资源的独占访问, 每次只能有一个线程对 Lock 对象
加锁, 线程开始访问共享资源之前应先获得 Lock 对象.
3,Reentrantl _ock 类实现了 Lock , 它拥有与 synchronized 相同的并发性和
内存语义, 在实现线程安全的控制中, 比较常用的是 Reentrantl _ock, 可以
显式加锁, 释放锁.
4,synchronized 与 Lock 的对比:
(1)Lock 是显式锁(手动开启和关闭锁, 别忘记关闭锁),synchronized 是隐式锁, 出了作用域自动释放.
(2)Lock 只有代码块锁, synchronized 有 代码块锁和方法锁.
(3)使用 Lock 锁, JVM 将花费较少的时间来调度线程, 性能更好. 并且具有更好的扩展性(提供更多的子类).
(4)优先使用顺序: Lock→同步代码块(已经进入了方法体, 分配了相应资源)→同步方法(在方法体之外).
来源: http://www.bubuko.com/infodetail-3384347.html