栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java多线程学习系列(六)之线程同步

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java多线程学习系列(六)之线程同步

Java多线程

线程同步
  • Java多线程
  • 一、同步代码块
    • 1.原子操作概念
    • 2.同步代码块
      • 2.1 同步代码块
      • 2.2 同步方法
    • 3. 线程同步的特征
    • 4. 同步与并发
    • 5. 线程安全的类
      • 5.1 可变了类和不可变类
        • 5.1.1 不可变类
        • 5.1.2 可变类
        • 5.1.3 可变类的线程安全问题
    • 6. 释放对象的锁
    • 7. 死锁


一、同步代码块 1.原子操作概念

  原子操作由相关的一组操作完成,这些操作可能会操作与其他线程共享的资源。为了保证得到正确的运算结果,一个线程在执行原子操作的期间,应该采取措施使得其他线程不能操作共享资源。

2.同步代码块 2.1 同步代码块

  为了保证每个线程能够执行原子操作,Java引入了同步机制,具体做法时再代表原子操作的程序代码前加上synchronized标记,这样的代码被称为同步代码块:

public String pop() {
        synchronized (this) {
            String goods = buffer[point];
            buffer[point] = null;
            Thread.yield();
            point--;
            return goods;
        }
    }

  每个java对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。当消费者线程试图执行以上带有头synchronized(this)标记的代码块时,消费者线程必须首先获得this关键字引用的Stack对象的锁。在一下两种情况,消费者线程会有着不同的命运:

  1. 假如这个锁对象已经被其他线程占用,java虚拟机就会把这个消费者线程放到Stack对象的锁池中,消费者线程进入阻塞状态。在Stack对象的锁池中可能会有许多等待锁的线程。等到其他线程释放了锁,Java虚拟机会从锁池中随机取出一个线程,使这个线程拥有锁,并转到就绪状态
  2. 加入这个锁没有被其他线程占用,消费者线程就会获得这把锁,开始执行同步代码块。一般情况下,消费者线程只有执行完同步代码块,才会释放锁。

Tips

对象的同步锁只是概念上的一种锁,也可以称为以一个对象为标记的锁。

2.2 同步方法

  如果一个方法中的所有代码都属于同步代码,那么可以直接在方法前用synchronized修饰。下面两种方式等价:

public synchronized String pop() {
            String goods = buffer[point];
            buffer[point] = null;
            Thread.yield();
            point--;
            return goods;
    }
public String pop() {
        synchronized (this) {
            String goods = buffer[point];
            buffer[point] = null;
            Thread.yield();
            point--;
            return goods;
        }
    }
3. 线程同步的特征

Tips

所谓线程之间保持同步,是指不同的线程在执行同一个对象的同步代码块时,因为要获得这个对象的锁而互相牵制。

1.如果一个同步代码块和非同步代码块操作共享资源,这仍然会造成对共享资源的竞争。 因为当一个线程执行一个对象的同步代码块时,其他线程仍谈可以执行对象的非同步代码块。
2.每个对象都有唯一的同步锁。
3.静态方法前也可以用synchonized修饰符。
4.当一个线程开始执行同步代码块,并不意味着必须以不中断的方式运行。进入同步代码块的线程也可以执行Thread.Sleep()或者Thread.yield()方法,此时并没有释放锁,只是把运行机会(即CPU)让给了其他的线程。
5.synchonized声明不会被继承。如果一个用synchonized修饰的方法被子类覆盖,那么子类中这个方法不在保持同步,除非也用synchonized修饰。

4. 同步与并发

  同步时解决共享资源竞争的有效手段,当一个线程已经在操作共享资源时,其他线程只能等待,只有当已经在操作共享资源的线程执行完同步代码块时,其他线程才有机会操作共享资源。
  多线程的同步与并发是一对此消彼长的矛盾。

5. 线程安全的类

一个线程安全的类满足以下条件:

  1. 这个类的对象可以同时被多个线程安全的访问
  2. 每个线程都能正常执行原子操作,得到正确的结果
  3. 每个线程的原子操作完成后,对象处于逻辑上的合理状态
5.1 可变了类和不可变类 5.1.1 不可变类

  不可变类,是指当创建了这个类的实例以后,就不允许修改它的属性值。在JDK的基本类库中,所有基本类型的包装类,如Integer和Long类,都是不可变类,java.lang.String也是不可变类。不可变类总是线程安全的

5.1.2 可变类

  可变类的实例允许修改。

5.1.3 可变类的线程安全问题

可变类的线程安全往往以降低并发性能为代价,为了减少这一负面影响,可以采取以下措施:

  1. 只对导致资源竞争的代码进行同步。
  2. 如果一个可变类有两种云运行环境:单线程运行环境和多线程运行环境,那么可以为这个类提供两种实现,在单线程运行环境中适用未采取同步的类的实现,在多线程运行环境中适用采取同步的类的实现。所谓的单线程运行环境,是指类的对象只会被一个线程访问。所谓多线程的运行环境,是指类的同一个对象会被多个线程同时访问。
6. 释放对象的锁

在以下情况,持有锁对象的线程会释放锁:

  • 执行完同步代码块,就会释放锁
  • 在执行同步代码块的过程中,遇到了异常而导致线程终止,锁也会被释放
  • 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进入对象的等待池。
    在以下情况,持有锁对象的线程不会释放锁:
  • 在执行同步代码块的过程中,执行了Threa.sleep()方法,当前线程放弃CPU开始睡眠,在睡眠中不会释放锁。
  • 在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁
  • 在执行同步代码块的过程中,其他线程执行了当前对象的suspend()方法,当前线程被暂停,但是不会释放锁(Thread.suspend()已经被废弃)
package main.com.feng.releaselock;

public class Machine extends Thread {
    private int a = 1;
    public synchronized void prin() {
        System.out.println("a=" + a);
    }

    @Override
    public void run() {
        synchronized (this) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            a=1/0; // 异常 ArithmeticException
            a++;
        }
    }

    public static void main(String[] args) {
        Machine machine = new Machine();
        machine.start();
        Thread.yield();
        machine.prin();
    }
}

以上代码执行流程:

1.主线程执行Thread.yield()方法,把CPU让给machine
2.machine线程获得machine对象的锁,开始执行run方法中的同步代码块。
3.machine线程执行Thread.sleep(2000),machine线程放弃cpu,开始睡眠,但是不会释放machine对象的锁。
4.主线程获得CPU,试图运行machine.print()方法,由于主线程无法获得machine对象的锁,因此进入machine对象的锁池,等待machine线程释放锁。
5.machine线程睡眠结束,执行"1/0"操作,导致ArithmeticException异常,machine释放锁,并且异常终止。
6.主线程获得锁及CPU,执行machine.print()方法。

7. 死锁

  当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。

package main.com.feng.deadlock;

public class Machine extends Thread {
    private Counter counter; // 共享数据

    public Machine(Counter counter) {
        this.counter = counter;
        start(); // 启动自身线程
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.add();
        }
    }

    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        counter1.setFriend(counter2);
        counter2.setFriend(counter1);

        Machine machine1 = new Machine(counter1);
        Machine machine2 = new Machine(counter2);

    }
}
class Counter{
    private int a;
    private Counter friend;
    public void setFriend(Counter frined) {
        this.friend = frined;
    }
    public synchronized void add() {
        a++;
        Thread.yield();
        friend.delete();
        System.out.println(Thread.currentThread().getName() + ":add");
    }
    public synchronized void  delete() {
        a--;
        System.out.println(Thread.currentThread().getName()+ "delete");
    }
}

以上代码执行流程:

1.machine1线程获得CPU和counter1对象的锁,开始i执行counter1.add()方法。
2.machine1线程执行完a++操作,然后执行Thread.yield()方法,machine1线程放弃CPU但是不会释放counter1对象的锁
3.machine2线程获得CPU和counter2对象锁,开始执行counter2.add()方法。
4.machine2线程执行完“a++"操作,然后执行Thread.yield()方法,machine2线程放弃CPU,但是不会释放counter2对象锁.
5.machine1线程获得CPU,试图执行counter2.delete()方法。由于counter2对象锁已经被占用,machine1线程放弃CPU,进入counter2对象的锁池,等待machine2线程释放counter2对象锁。
6.machine2线程获得CPU,试图运行counter1.delete()方法。由于counter1对象的锁已经被占用,machine2线程放弃CPU,进入counter对象的锁池,等待machine1线程释放counter1对象的锁。
  machine1和machine2线程分别持有counter1和counter2对象的锁,都在等待对方的锁,死锁就这样产生,这两个线程都被阻塞,谁都无法恢复运行。

注:学习内容和代码,来源自《JAVA 面向对象编程》(第二版)作者:孙卫琴

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1003752.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号