我们编写的程序需要放到一个多线程的环境下运行,更需要关注的是这些数据在多线程环境下运行是否安全。
多行程在以下三个条件存在时会存在安全问题
条件1.多线程并发
条件2.有共享数据
条件3.共享数据有修改行为
Tip:
局部变量是不存在安全问题的,因为永远不会共享(局部变量在栈中)
常量也不存在安全问题,因为不可被修改
堆和方法区都是多线程共享的,所以可能存在线程安全问题
如果使用局部变量的话:
建议使用StringBuilder,因为局部变量不存在线程安全问题
解决线程的安全问题可以通过线程排队执行(不能并发)
这种机制被称为:线程同步机制
异步编程模型:
线程t1和线程t2,各自执行各自的,谁也不用管谁
异步(并发,效率较高)
同步编程模型:
线程t1和线程t2,在线程t1(t2)执行的时候,必须等待t2(t1)执行结束
俩个线程之间发生等待关系
同步(排队,效率低,安全)
同步代码块
关键字synchronized() 线程同步机制,线程排队执行
括号中的数据必须是多线程共享的数据
在java语言中,任何一个对象都有“一把锁”(标记)
代码举例:
创建银行账户类:
public class Account {
private String name;
private double money;
public Account() {
}
public Account(String name, double money) {
this.name = name;
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
//取款方法
public void withdraw(double money){
//同步代码块
//关键字synchronized 线程同步机制,线程排队执行
//括号中的数据必须是多线程共享的数据
//这里的共享对象是账户对象
synchronized (this) {
Double befor = this.money;
double after = befor - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setMoney(after);
}
}
}
创建线程来模拟取款
public class ThreadMoney extends Thread{
private Account act;
//通过构造方法传递过来的账户对象
public ThreadMoney(Account act) {
this.act = act;
}
public void run(){
double money = 5000;
act.withdraw(money);
System.out.println(Thread.currentThread().getName() +
"取了" + money + ",剩余" + act.getMoney());
}
}
测试类:创建两个线程同时取款
public class Test {
public static void main(String[] args) {
Account act=new Account("lisi",10000);
//线程t1
Thread t1=new ThreadMoney(act);
//线程t2
Thread t2=new ThreadMoney(act);
t1.start();
t2.start();
}
}
结果:
Thread-0取了5000.0,剩余5000.0
Thread-1取了5000.0,剩余0.0
t1和t2在执行期间进行了线程同步(排队)
若没有同步,可能会出现以下情况:
Thread-0取了5000.0,剩余5000.0
Thread-1取了5000.0,剩余5000.0
原因是什么呢?
t1、t2同时执行,同时取钱(异步),这时候原来有10000,二者都取了5000之后才setMoney()修改余额,
两次修改都是从10000——>5000,所以最后返回余额当然是5000(但实际上两者都取了5000)。
这就肯定不对了。所以必须要用到synchronized线程同步机制
面试题:问t1线程启动后,t2线程会不会等待t1线程先执行完再执行?
不会,因为t2线程执行的是doOther方法,doOther方法并没有被synchronized锁住
代码如下:
public class Test1 {
public static void main(String[] args) {
Myclass mc=new Myclass();
Thread t1=new MyThread(mc);
Thread t2=new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);//保证t1先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread{
private Myclass myclass;
public MyThread(Myclass myclass) {
this.myclass = myclass;
}
public void run(){
if(Thread.currentThread().getName().equals("t1"))
myclass.doSome();
if(Thread.currentThread().getName().equals("t2"))
myclass.doOther();
}
}
class Myclass{
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
俩个线程t1、t2同时共享o1,o2两个对象
t1先锁o1,再锁o2
t2先锁o2,再锁o1
此时如果t1锁住o1,t2锁住o2,会陷入僵持。
代码实例:
public class Deadlock {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
Thread t1=new MyThread1(o1,o2);
Thread t2=new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
public void run(){
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
public void run(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
第一种:
同步代码块
synchronized(线程共享对象){
同步代码块;
}
第二种:
在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块一定是方法体
第三种:
在静态方法上使用synchronized
表示找类锁
类锁永远只有一把
第一种:尽量使用局部变量代替“实例变量和静态变量”
第二种:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应一个对象)
第三种:如果不能使用局部变量,对象也不能创建多个,这时候就只能选择synchronized了(线程同步机制)
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的是:垃圾回收机制(守护线程)
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束在守护线程执行前(.start())写上:setDaemon(true);
作用:间隔特定的时间,执行特定的程序
java中已经写好了一个定时器:java.util.Timer(开发中很少用)
实际开发中使用较多的是Spring框架中提供的SpringTask框架。
实现Callable接口(JDK8新特性)
这种方式实现的线程可以获取线程的返回值。
之前两种:继承Thread、继承Runnable 这两种方法没有返回值。
什么是“生产者和消费者模式”?
生产线程负责生产,消费线程负责消费。
生产线程和消费线程要达到均衡
这是一种特殊的业务要求,在这中特殊的情况下使用wait方法和notify方法
wait和notify方法建立在线程同步基础之上,因为多线程要同时操作一个仓库,有线程安全问题。
模拟这样一个需求:
仓库采用List集合
List集合中假设只能存1个元素。
1个元素表示仓库满了
如果List集合中元素个数为0就表示仓库空了。
生产一个消费一个
代码实例:
生产线程:
//生产线程
class Producer implements Runnable{
//仓库
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
//一直生产(死循环模拟一直生产)
while (true){
//给仓库对象上锁
synchronized (list) {
if (list.size() > 0) {//大于0说明仓库满了(最多存一个)
//暂停生产
//当前线程进入等待状态,并且释放list集合的锁
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//若仓库没满,可以生产
Object obj=new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName()+"--->"+obj);
//唤醒消费者进行消费
list.notify();
}
}
}
}
消费线程:
//消费线程
class Consumer implements Runnable{
//仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
//一直消费
while (true){
//给仓库对象上锁
synchronized (list){
//仓库若已经空了
if(list.size()==0){
//暂停消费
//当前线程进入等待状态,并且释放list集合的锁
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//若仓库没空,进行消费
Object obj=list.remove(0);
System.out.println(Thread.currentThread().getName()+"--->"+obj);
//唤醒生产者生产
list.notify();
}
}
}
}
测试类:
public class ThreadTest {
public static void main(String[] args) {
//创建一个仓库对象
List list=new ArrayList();
//创建两个线程对象
//生产者
Thread t1=new Thread(new Producer(list));
//消费者
Thread t2=new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
实现结果:
生产一个消费一个,并不多生产,也不超消费。
生产者线程--->java.lang.Object@eb6ad02
消费者线程--->java.lang.Object@eb6ad02
生产者线程--->java.lang.Object@34ac4d0c
消费者线程--->java.lang.Object@34ac4d0c
生产者线程--->java.lang.Object@46369de7
消费者线程--->java.lang.Object@46369de7
…………