目录
1、线程的同步问题
1.1、线程的同步与异步
同步是执行或调用一个方法时,每次都需要拿到对应的结果才会继续往后执行;异步与同步相反,它会在执行或调用一个方法后就继续往后执行,不会等待获取执行结果。二者的区别就是处理请求发出后,是否需要等待请求结果,再去继续执行其他操作。
线程安全是同步,线程不安全是异步
同步:排队执行,效率低但是安全
异步:同时执行,效率高但是数据不安全
线程同步:
1.2、实现线程同步的几种方式
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
实现线程同步的7种方式:
(1)同步代码块
(2)同步方法
(3)使用重入锁实现线程同步(ReentrantLock)
(4)使用阻塞队列实现线程同步(BlockingQueue (常用)add(),offer(),put()
(5)使用特殊域变量(volatile)实现同步(每次重新计算,安全但并非一致)
(6)使用局部变量实现线程同步(ThreadLocal)以空间换时间
(7)使用原子变量实现线程同步(AtomicInteger(乐观锁))
2、同步代码块用例子实现线程同步
同步代码块就是把操作共享数据的代码锁起来
格式:
synchronized(锁){
操作共享数据的代码
}
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
注意:
(1)synchronized(锁)不要写在循环的外面,因为线程一但进入synchronized(锁)中,就会上锁会在里面的循环执行完全部才会退出synchronized(锁)并释放锁,这就会导致这时候所有任务都被这个线程做完了,其他线程进来也没任务做了
(2)synchronized(锁)中的锁对象必须是唯一的,如果每个线程的锁不是同一个的话,就起不到锁的作用。synchronized(this)这里的this代表的是当前进来的线程,当把锁对象定义为this时,因为每个线程是不同的,所以this代表的锁对象也不是唯一的。所以一般会把当前类的字节码文件,因为类的字节码文件是唯一的,在同一个文件里只能有一个与文件同名的Class文件,即下面需求的代码可以写成synchronized(MyThread.class),括号里面写当前类的字节码文件。
比如我现在有这么一个需求:某电影院目前正在上映新电影,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
2.1、线程不同步的情况
创建线程类,创建多个此线程进行同时出售电影票
public class MyThread extends Thread{
//表示这个线程类所有的对象,都共享ticket数据
static int ticket = 0;//0 ~ 99
@Override
public void run() {
while (true) {
if (ticket < 50) {
try {
//设置线程睡眠
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
创建线程对象,然后启动线程
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//给线程命名
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
运行结果
从运行的结果可以发现,卖出去的电影票重复了,并且还超出范围了
总结:从运行的结果可以发现,卖出去的电影票重复了,并且还超出范围了,这是因为线程执行时时具有随机性的,比如当线程1运行sleep()方法后,CUP的执行权会落到线程2或线程3的手中,这时候就有可能发生多个线程同时执行到了 tickte++; 这条语句,然后它们在买票时的票号(ticket)会是一样的,即打印出来的 ticket 是一样的,而最后电影票的票号超出范围也是这个原因
2.2、线程同步的代码
只需要同步代码块:把MyThread中操作共享数据的代码用 synchronized 锁起来就可以了
public class MyThread extends Thread{
//表示这个线程类所有的对象,都共享ticket数据
static int ticket = 0;//0 ~ 99
//锁对象,一定要是唯一的
static Object obj = new Object();
@Override
public void run() {
while (true) {
//同步代码块
synchronized (obj) {
if (ticket < 50) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
}
3、同步方法实现线程同步
同步方法就是把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数) {
. . . . . .
}
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定,如果方法是非静态的,那么它的锁对象是this,就是当前方法的调用者;如果是静态方法,那么锁对象是当前类的字节码文件对象
现在改上面的需求为:某电影院目前正在上映新电影,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票,利用同步方法完成
实现代码如下:
public class MyRunnable implements Runnable{
int ticket = 0;
//现在实现线程的方式是实现Runnable接口的方式,在这种方式中的MyRunnable是作为一个参数让线程去执行的,
//所以我只会创建一次MyRunnable的对象,既然只创建一次,那么ticket前就不需要加static关键字来让线程共享它,
//这种方式下的几个线程本就是共享ticket的
@Override
public void run() {
//1.循环
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.同步代码块(同步方法)
if (method()) break;
}
}
//这里的method()方法是非静态的,这里的对象锁就是this,
//这个this就是在测试类中创建的MyRunnable对象,而在测试类中我只会创建一次MyRunnable的对象,
//所以在测试类中的MyRunnable对象是唯一的,即锁对象就是唯一的
private synchronized boolean method() {
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket == 50) {
return true;
} else {
//4.判断共享数据是到了末尾,如果没有到末尾
ticket++;
System.out.print(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
if (ticket%2 == 0) {
System.out.println();
}
}
return false;
}
}
代码中需要注意的点:
(1)现在这里实现线程的方式是实现Runnable接口的方式,在这种方式中的MyRunnable是作为一个参数让线程去执行的,所以我只会创建一次MyRunnable的对象,既然只创建一次,那么ticket前就不需要加static关键字来让线程共享它,这种方式下的几个线程本就是共享ticket的
(2)这里的method()方法是非静态的,这里的对象锁就是this,这个this就是在测试类中创建的MyRunnable对象,而在测试类中我只会创建一次MyRunnable的对象,所以在测试类中的MyRunnable对象是唯一的,即锁对象就是唯一的
创建线程对象,然后启动线程
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"窗口1");
Thread t2 = new Thread(mr,"窗口2");
Thread t3 = new Thread(mr,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
4、重入锁(ReentrantLock)实现线程同步
Lock锁:synchronized属于隐式锁,即锁的持有与释放都是隐式的,虽然可以理解同步代码块和同步方法的锁对象问题,但是并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock(显式锁)
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法:void lock():获得锁,手动上锁;void unlock():释放锁,手动释放锁
Lock是接口不能直接实例化,可以使用它的实现类ReentrantLock,ReentrantLock的一个重要特点是,它允许一个线程多次获取同一个锁而不会产生死锁。这与synchronized关键字提供的锁定机制非常相似,但ReentrantLock提供了更高的扩展性。
这里用ReentrantLock的构造方法ReentrantLock():创建一个ReentrantLock的实例
public class MyThread extends Thread{
//表示这个线程类所有的对象,都共享ticket数据
static int ticket = 0;//0 ~ 99
//锁对象,一定要是唯一的
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//同步代码块
lock.lock();
try {
if (ticket < 50) {
Thread.sleep(100);
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
运行结果
推荐: