- 线程的同步产生原因
- 线程的同步处理操作
- 线程的死锁
1. 同步问题的引出
同步指的是多个线程访问同一资源时需要了解的情况。
非同步情况下的操作:
package com.company;
class MyThread implements Runnable{
private int ticket = 5;
public void run(){
for(int x=0;x<20;x++){
if(this.ticket>0)
System.out.println(Thread.currentThread().getName()+"sellticket:"+ticket--);
}
}
}
public class Main {//主类
public static void main(String args[]) throws Exception {
MyThread mt = new MyThread();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
new Thread(mt,"D").start();
}
}
此时没有出现什么问题是因为我们在一个JVM进程下运行,并且没有受到任何的影响。如果要想观察到问题可以加入一个延迟来观察
package com.company;
class MyThread implements Runnable{
private int ticket = 5;
public void run(){
try {
for(int x=0;x<20;x++){
if(this.ticket>0)
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"sellticket:"+ticket--);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class Main {//主类
public static void main(String args[]) throws Exception {
MyThread mt = new MyThread();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
new Thread(mt,"D").start();
}
}
结果出现了负数,这就是不同步的情况。到底是如何造成不同步,整个买票的过程分为两步完成:
- 第一步:判断是否有剩余票数
- 第二部:票数减少
由于每一个线程进入的时候存在一个sleep的阶段,来不及改变ticket,下一个线程就进入访问了,此时ticket都为1,所以“第一道防线”都被突破,都可以进入到ticket–的过程,所以会出现问题。
但是现在的访问都是异步操作,不能同步否则好多人访问一个页面,每个人都要等待的话,最后一个人得等多久啊!
2. 同步处理
如何进行同步呢?
- 通过观察发现以上程序所带来的最大问题是判断和修改数据是分开完成的,即某几个线程可以同时实现这些功能。
- 把两步操作当作整体来看,当一个线程进入时,如果发现此处有有线程正在进行,该进程需要等待。确保整个过程中只有一个线程访问资源。
- 在Java中如果想要使用线程的同步,可以使用synchronized关键字,这个关键字可以通过两种方式使用:
- 同步代码块
- 同步方法
Java中有四种代码块:普通代码块、构造块、静态块、同步块。
观察同步块:(必须要有一个锁定的对象,一般该对象为this)
package com.company;
class MyThread implements Runnable{
private int ticket = 5;
public void run(){
for(int x=0;x<20;x++) {
synchronized (this) {//当前操作每次只允许一个对象进入
if (this.ticket > 0) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "sellticket:" + ticket--);
}
}
}
}
}
public class Main {//主类
public static void main(String args[]) throws Exception {
MyThread mt = new MyThread();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
new Thread(mt,"D").start();
}
}
注意:try catch的位置不对也会对同步造成影响
但是更愿意用同步方法:
package com.company;
class MyThread implements Runnable {
private int ticket = 5;
public void run() {
for (int x = 0; x < 20; x++) {
this.sale();//调用同步方法
}
}
public synchronized void sale() {
//同步方法
if (this.ticket > 0) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "sellticket:" + ticket--);
}
}
}
public class Main {//主类
public static void main(String args[]) throws Exception {
MyThread mt = new MyThread();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
new Thread(mt,"D").start();
}
}
多个线程访问同一个资源的时候肯定需要同步
异步操作的执行速度要高于同步操作,但是同步操作时数据的安全性较高,属于线程安全的线程操作。
3. 死锁
通过分析发现所谓的同步就是一个线程对象等待另一个线程对象执行完毕后的操作形式。
下面程序没有任何意义,只是希望大家看一下死锁产生的具体特征。
class A {
public synchronized void say(B b){
System.out.println("I say:把你的本给我,我给你笔,否则不给");
b.get();
}
public synchronized void get(){
System.out.println("I say:付出了笔,得到了本,但还是什么都干不了");
}
}
class B {
public synchronized void say(A a){
System.out.println("you say:把你的笔给我,我给你本,否则不给");
a.get();
}
public synchronized void get(){
System.out.println("you say:付出了本,得到了笔,但还是什么都干不了");
}
}
public class Main implements Runnable {//主类
public static A a = new A();
public static B b = new B();
public static void main(String args[]) throws Exception {
new Main();
}
public Main(){
new Thread(this).start();
b.say(a);
}
public void run(){
a.say(b);
}
}
死锁是在程序开发过程中某种逻辑错误所造成的问题,并不是简单的问题
面试题:请解释多个线程访问同一资源时需要考虑到哪些情况?有可能带来哪些问题?
- 多个线程访问同一资源时一定要处理好同步,可以使用同步代码块,或同步方法来解决
- 但过多的使用同步,有可能造成死锁
总结:
- 最简单的理解同步和异步的操作那么就可以通过synchronized来实现
- 死锁是一种不定的状态
生产者和消费者module
- 生产者和消费者问题的产生
- Object类对多线程问题支持
生产者和消费者是指两个不同的线程类对象,操作同一资源的情况,具体操作流程如下:
- 生产者负责生产数据,消费者负责取走数据
- 生产者每生产完一组数据,消费者就要取走一组数据
那么现在假设要生产的数据如下:
- 第一组数据 :title = Taylor content= goodstudent
- 第二组数据 :title = animal content = monkey
生产者类和消费者类都继承Runnable接口,然后指向同一个数据类型
现在实际上通过以上的代码可以发现两个严重问题: - 第一个是:数据错位
- 第二个是:数据重复取出,数据重复设置(数据还没取走呢,就开始设置数据了)
package com.company;
import javax.lang.model.element.NestingKind;
class Info{
String title;
String content;
public void setTitle(String title)
{
this.title=title;
}
public String getTitle(){
return title;
}
public void setContent(String content)
{
this.content=content;
}
public String getContent(){
return content;
}
}
class Productor implements Runnable {
private Info info;
public Productor(Info info) {
this.info = info;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
this.info.setTitle("taylor");
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
this.info.setContent("goodstudent");
} else {
this.info.setTitle("animal");
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
this.info.setContent("monkey");
}
}
}
}
class Consumer implements Runnable{
private Info info;
public Consumer(Info info){
this.info=info;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(this.info.getTitle()+"-"+this.info.getContent());
}
}
}
public class Main {//主类
public static void main(String args[]) throws Exception {
Info info = new Info();
new Thread(new Productor(info)).start();
new Thread(new Consumer(info)).start();
}
}
解决问题的方法
数据的错位完全是因为非同步的操作所造成的,所以应该同步处理。因为取和设置是两个类,要想进行同步设置需要其定义在一个类里面完成。
第一个问题的解决
package com.company;
import javax.lang.model.element.NestingKind;
class Info{
String title;
String content;
public synchronized void set(String title,String content){
this.content=content;
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
this.title=title;
}
public synchronized void get(){
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(this.title+"-"+this.content);
}
}
class Productor implements Runnable {
private Info info;
public Productor(Info info) {
this.info = info;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
info.set("taylor","goodstudent");
} else {
info.set("animal","monkey");
}
}
}
}
class Consumer implements Runnable{
private Info info;
public Consumer(Info info){
this.info=info;
}
@Override
public void run() {
for(int i=0;i<100;i++){
info.get();
}
}
}
public class Main {//主类
public static void main(String args[]) throws Exception {
Info info = new Info();
new Thread(new Productor(info)).start();
new Thread(new Consumer(info)).start();
}
}
此时数据错位的问题得到了解决,但是重复操作的问题更加严重了,因为同步只能保证当前操作的完整性,至于操作的其他逻辑处理不了。
第二个问题的解决
如果要想实现整个代码的操作,必须加入等待与唤醒机制,才能去解决这样的问题,在Object类中有方法可以解决这个问题
public final void wait(long timeout, int nanos)
throws InterruptedException
这里面还有一个重载,这里可以加一个唤醒机制
- 等待:
public final void wait() throws InterruptedException
- 唤醒第一个等待线程:
public final void notify()
- 唤醒全部等待线程,哪个优先级高哪个先执行:
public final void notifyAll()
,(优先级就是干这事er的)
package com.company;
import javax.lang.model.element.NestingKind;
class Info{
private String title;
private String content;
private boolean flag=true;
//flag = true;表示可以生产但不可以取走
//flag = false;表示可以取走但是不可以生产
public synchronized void set(String title,String content){
//重复进入到Set方法里面,发现不能生产,所以要等待
if(this.flag == false)
try {
super.wait();
} catch (Exception e) {
e.printStackTrace();
}
this.content=content;
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
this.title=title;
this.flag = false;//生产完成后要修改生产标记
//修改生产标记之后有可能有等待线程
//唤醒其他等待线程
super.notifyAll();
}
public synchronized void get() {
if (this.flag == true) {//还没生产呢!
try {
super.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(this.title + "-" + this.content);
this.flag=true;//修改生产标记
}
}
class Productor implements Runnable {
private Info info;
public Productor(Info info) {
this.info = info;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
info.set("taylor","goodstudent");
} else {
info.set("animal","monkey");
}
}
}
}
class Consumer implements Runnable{
private Info info;
public Consumer(Info info){
this.info=info;
}
@Override
public void run() {
for(int i=0;i<100;i++){
info.get();
}
}
}
public class Main {//主类
public static void main(String args[]) throws Exception {
Info info = new Info();
new Thread(new Productor(info)).start();
new Thread(new Consumer(info)).start();
}
}
面试题:请解释sleep与wait的区别?
- sleep是Thread类定义的方法,wait是Object类定义的方法;
- sleep可以设置我们的休眠时间,时间一到自动唤醒,而wait需要等待notif进行唤醒
总结
这是一个非常经典的多线程的处理模型,可以更加理解Object类的作用。