先看Demo1:
public class Demo1_Synchronized {
public static void main(String[] args) {
final Printer p = new Printer();
new Thread() {
public void run() {
for (int i = 0; i < 100; ++i) {
p.print1(); // jdk1.8不需要显式将p用final修饰,默认用final修饰
}
}
}.start();
new Thread() {
public void run() {
for (int i = 0; i < 100; ++i) {
p.print2();
}
}
}.start();
}
}
class Printer {
// 零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:
// 生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
private byte[] lock = new byte[0]; // 特殊的锁
// 注意:匿名对象不可以当做锁对象,因为不能保证两个锁对象是同一个对象
// 非静态的同步方法,锁对象是this,锁方法和锁this是一样的效果
// 静态的同步方法,锁对象是当前类的字节码对象,锁方法和锁Printer.class是一样的
public static void print1() {
// synchronized (lock) {
synchronized (Printer.class) {
System.out.print("我");
System.out.print("最");
System.out.print("帅");
System.out.print("\r\n");
}
// }
}
public synchronized static void print2() {
// synchronized (lock) {
System.out.print("你");
System.out.print("很");
System.out.print("丑");
System.out.print("\r\n");
// }
}
}
注意:做好不要把匿名对象不可以当做锁对象,因为不能保证两个锁对象是同一个对象,这样就只能锁住类了。
非静态的同步方法,锁对象是this,锁方法和锁this是一样的效果
比如public synchronized void print(){...}
就和public void print(){
synchronized(this){...}
}
效果是一样的。
静态的同步方法,锁对象是当前类的字节码对象,锁方法和锁Printer.class是一样的,相当于给当前的类加锁
public synchronized static void print(){...}
和public static void print(){
synchronized(Printer.class){...}// 类名.class
}
效果是一样的。
经验总结:
如果是多个线程依赖(聚合)相同Runnable实现类对象instance,那么多个线程共同操作这个变量时,这个变量是不需要加static的,锁住当前this即可保证线程安全。如下:
public class MainClass implements Runnable {
static MainClass instance = new MainClass();
volatile int i = 0;
public synchronized void increase() {
++i;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
}
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 10000; ++i) {
increase();
}
System.out.println(i);
}
}
如果是多个线程依赖(聚合)不同Runnable实现类对象,那么多个线程操作各自Runnable依赖对象里面的变量时,这个变量是需要加上static的才能保证这个变量是不同线程共享的(因为static本来就是修饰共享变量,否则不同对象里面的对象互不共享),再锁住这个静态方法,可以正确同步,保证线程安全。如下:
注意:变量共享的static的概念和多线程无关,并不是多线程独有,只不过这里运用到一起了。
public class MainClass implements Runnable {
static MainClass instance = new MainClass(); // 这里static语法需要,为了在main使用
static volatile int i = 0;
public static synchronized void increase() {
++i;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MainClass());
Thread t2 = new Thread(new MainClass());
t1.start();
t2.start();
t1.join();
t2.join();
}
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 10000; ++i) {
increase();
}
System.out.println(i);
}
}
这个知识点更简洁的例子写在代码仓库,可以见这里
Runnable例子
来看下一个Demo2
public class Demo2_Ticket {
/**
* @param args
* 模拟铁路售票, 4个窗口卖100张票
*/
public static void main(String[] args) {
for (int i = 0; i < 4; ++i) {
new TicketSeller("窗口" + i).start();
}
}
}
class TicketSeller extends Thread {
private static int tickets = 100;
public TicketSeller(String name) {
super(name);
// TODO Auto-generated constructor stub
}
public void run() {
while (true) {
synchronized (TicketSeller.class) { // ==========提问点提问点提问点======
if (tickets == 0) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(getName() + "...这是第" + tickets-- + "号票");
}
}
}
}
模拟铁路卖票100张,肯定不能重复卖票,不然上车该争车位发生矛盾了。
看到提问点
如果这里不加synchronized,可以吗?
不可以,如果不加,可能卖出负数号票,比如线程3刚要进行下一次循环,此时tickets=1,而线程2和线程1正好在执行tickets--,结果抢先在线程3之前执行了,tickets=-1了,然后就跳过了判断==0阶段,无限循环停不下来了。
那么问题来了,我判断条件改为tickets<=0不就好了?
不可以,哪怕这么改动(为了放大现象,加上了Thread.sleep(10)睡眠10ms),比如线程3, 2, 1都在睡眠,此时tickets=1,然后线程3睡醒了,先执行tickets--,打印这是第1号票,接着下一轮循环跳出,线程3结束,然后线程2和线程1睡醒了,依然执行tickets--,依次打印这是第0号票,这是第-1号票。。???卖出负数票了??
那么问题来了,我加上synchronized (this){...}不就好了?
不可以,这里是new Thread了4次,是4个线程对象,每个对象都有自己的this,互不干扰,这种方法等于没加synchronized。
那么问题来了,我加上锁对象就好了,private Object obj = new Object();再synchronized (obj) {...}
这个可以,不过这个是成员变量,每个线程对象都有自己的成员变量obj,所以要改为共享的,加上static。
不过零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码,生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。这里改为private static byte[] lock = new byte[0];再synchronized (lock) {...}会比较好。当然,锁字节码对象也可以。synchronized(TicketSeller.class){...}// 类名.class
那么看看Demo3,用Runnable实现
public class Demo3_Ticket {
/**
* @param args
* 多次开启一条线程是非法的
*/
public static void main(String[] args) {
Ticket t = new Ticket();
new Thread(t, "窗口1").start();
new Thread(t, "窗口2").start();
new Thread(t, "窗口3").start();
new Thread(t, "窗口4").start();
}
}
class Ticket implements Runnable {
private int tickets = 100;
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
synchronized (this) { // ===========提问点提问点提问点=========
if (tickets == 0) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "...这是第" + tickets-- + "号票");
}
}
}
}
看到提问点
在这里用synchronized (this) {...}对吗?
是对的,这里4个线程都是用的同一个Ticket对象。里面的tickets不需要加static,因为这个代码块同时只能一个线程执行,不会有并发问题。也可以synchronized (Ticket.class) {...}。
这里的synchronized (this) {...}能和上面一句while (true) {...}交换吗?
可以,但是!就只有窗口1在售票(即线程1执行),执行完才轮到窗口2,然后发现tickets已经为0,线程2结束,同理,线程3,4结束。程序结束。
我们要避免死锁问题,我们简化一下哲学家的例子,一个人吃饭,习惯先拿左筷子,另一个人习惯先拿右筷子,每个人拿起一只筷子就不会放下,除非吃完一顿后才放下一双筷子供其他人使用。
见下面一个例子Demo5:
public class Demo4_DeadLock {
/**
* 尽量避免同步代码块的嵌套
*/
private static String s1 = "筷子左";
private static String s2 = "筷子右";
public static void main(String[] args) {
new Thread() {
public void run() {
while (true) {
synchronized (s1) {
System.out.println(getName() + "获取"+ s1 + "等待" + s2);
synchronized (s2) {
System.out.println(getName() + "获取"+ s2 + "开始吃饭");
}
}
System.out.println("吃完了,放下一双筷子");
}
}
}.start();
new Thread() {
public void run() {
while (true) {
synchronized (s2) {
System.out.println(getName() + "获取"+ s2 + "等待" + s1);
synchronized (s1) {
System.out.println(getName() + "获取"+ s1 + "开始吃饭");
}
}
System.out.println("吃完了,放下一双筷子");
}
}
}.start();
}
}
运行结果:
结果互相等待,拿着左筷子的人等待右筷子,拿着右筷子的人等待左筷子。谁也不让谁,就造成了死锁。所以我们要避免synchronized的嵌套使用。
关于生产者消费者的例子见这里,博客比较难写,直接见仓库
生产者消费者的例子代码