多线程同步
卖票实例
需求:每个线程卖100张票
class Ticket extends Thread
{
private int num = 100;
public void run()
{
sale();
}
public void sale()
{
while(true)
{
if(num > 0)
{
System.out.println(Thread.currentThread().getName()+“...sale...”num--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{ //四个对象,每个对象都有100张票
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
需求:4个线程一起卖100张票
class Ticket implements Runnable
{
private int num = 100;
public void run()
{
sale();
}
public void sale()
{
while(true)
{
if(num > 0)
{
System.out.println(Thread.currentThread().getName()+“...sale...”num--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket(); //一个对象,所有一共只有100张票
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
线程安全问题的现象:
线程安全问题产生的原因:
- 多个线程在操作共享的数据;
- 操作共享数据的线程代码有多条;
当一个线程在执行操作共享数据的多条代码的过程中,其他线程参与了运算,就会导致线程安全问题的产生。
lock同步锁
解决思路:
将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码时,其他线程是不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在java中,同步代码块就可以解决这个问题。
同步代码块的格式:
synchronize(对象)
{
需要被同步的代码
}
同步的好处:
解决了线程的安全问题。
同步的弊端:
相对降低了效率,因为同步外的线程都会判断同步锁。
同步的前提:
同步中必须有多个线程并使用同一个锁。
例:
class Ticket implements Runnable//extends Thread
{
private int num = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)
{
if(num>0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()
+".....sale...."+num--);
}
}
}
}
}
同步函数
public synchronized void add(int num)
{
sum = sum + num;
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println("sum="+sum);
}
同步函数使用的锁是 this
同步函数和同步代码块的区别:
- 同步函数的锁是固定的this
- 同步代码块的锁是任意的对象
建议使用同步代码块
因为用this就固定了锁,没有灵活性。
验证静态同步函数的锁
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class来表示。
多线程下的单例模式
如何保证对象唯一性?
- 不允许其他程序用new创建该类对象。
- 在该类中创建一个本类实例。
- 对外提供一个方法让其他程序可以获取该对象。
步骤:
- 私有化该类的构造函数。
- 通过New在本类中创建一个对象。
- 定义一个公有的方法,将创建的对象返回。
class Single
{
private int num;
public int getNum()
{
return num;
}
public void setNum(int num)
{
this.num = num;
}
//实现对象唯一性
static Single s = new Single(); //创建本类类型
private Single(){}//私有化构造函数
public static Single getS()
{ //获取
return s;
}
}
public class SingleDemo
{
public static void main(String[] args)
{
Single t1 = Single.getS();
Single t2 = Single.getS();
t1.setNum(10);
t2.setNum(14);
System.out.println(t1.getNum());
System.out.println(t2.getNum());
}
}
死锁
死锁常见于:
- 同步的嵌套
- 线程间通讯(多个线程在处理同一资源,但是任务却不同)
饿汉式:
class Single//类一加载,对象就已经存在了。
{
private static Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
懒汉式:
class Single2//类加载进来,没有对象,只有调用了getInstance方法时,才会创建对象。
//延迟加载形式。
{
private static Single2 s = null;
private Single2(){}
public static Single2 getInstance()
{
if(s==null)
s = new Single2();
return s;
}
}
等待/唤醒机制:
涉及的方法:
- wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。
- notify():唤醒线程池中一个线程(任意).
- notifyAll():唤醒线程池中的所有线程。
这些方法都必须定义在同步中。因为这些方法是用于操作线程状态的方法。
必须要明确到底操作的是哪个锁上的线程。
为什么操作线程的方法wait notify notifyAll定义在了Object类中?
因为这些方法是监视器的方法。监视器其实就是锁。
锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
生产者/消费者模式(所生产者/多消费者问题)
while判断标记,解决了线程获取执行权后,是否要运行!
notifyAll解决了,本方线程一定会唤醒对方线程。
if判断标记,只有一次,会导致不该运行的程序运行了,出现了数据错误的情况。
notify()只能唤醒一个线程,如果本方唤醒了本方,没有意义,而且while判断标记+notify会导致死锁。