线程同步
- 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对象的锁。在一下两种情况,消费者线程会有着不同的命运:
- 假如这个锁对象已经被其他线程占用,java虚拟机就会把这个消费者线程放到Stack对象的锁池中,消费者线程进入阻塞状态。在Stack对象的锁池中可能会有许多等待锁的线程。等到其他线程释放了锁,Java虚拟机会从锁池中随机取出一个线程,使这个线程拥有锁,并转到就绪状态
- 加入这个锁没有被其他线程占用,消费者线程就会获得这把锁,开始执行同步代码块。一般情况下,消费者线程只有执行完同步代码块,才会释放锁。
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修饰。
同步时解决共享资源竞争的有效手段,当一个线程已经在操作共享资源时,其他线程只能等待,只有当已经在操作共享资源的线程执行完同步代码块时,其他线程才有机会操作共享资源。
多线程的同步与并发是一对此消彼长的矛盾。
一个线程安全的类满足以下条件:
- 这个类的对象可以同时被多个线程安全的访问
- 每个线程都能正常执行原子操作,得到正确的结果
- 每个线程的原子操作完成后,对象处于逻辑上的合理状态
不可变类,是指当创建了这个类的实例以后,就不允许修改它的属性值。在JDK的基本类库中,所有基本类型的包装类,如Integer和Long类,都是不可变类,java.lang.String也是不可变类。不可变类总是线程安全的
5.1.2 可变类可变类的实例允许修改。
5.1.3 可变类的线程安全问题可变类的线程安全往往以降低并发性能为代价,为了减少这一负面影响,可以采取以下措施:
- 只对导致资源竞争的代码进行同步。
- 如果一个可变类有两种云运行环境:单线程运行环境和多线程运行环境,那么可以为这个类提供两种实现,在单线程运行环境中适用未采取同步的类的实现,在多线程运行环境中适用采取同步的类的实现。所谓的单线程运行环境,是指类的对象只会被一个线程访问。所谓多线程的运行环境,是指类的同一个对象会被多个线程同时访问。
在以下情况,持有锁对象的线程会释放锁:
- 执行完同步代码块,就会释放锁
- 在执行同步代码块的过程中,遇到了异常而导致线程终止,锁也会被释放
- 在执行同步代码块的过程中,执行了锁所属对象的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();
}
}
以上代码执行流程:
7. 死锁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()方法。
当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。
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 面向对象编程》(第二版)作者:孙卫琴



