一、多线程数据安全
线程同步
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用
①同步方法
)同步非静态方法:synchronized放在方法声明中,表示整个方法为同步方法,锁定this对象
如果有一个线程进入了该方法,其他线程要想使用当前this对象的任何同步方法,都必须等待前一个线程执行完该同步方法之后
)同步static方法:synchronized放在static方法声明中,表示锁定该类的class对象(Xxx.class,是Class类型的,是描述一个类的信息的对象)
--如果有一个线程进入了该方法,其他线程要想使用当前类中的任何同步静态方法,都必须等待前一个线程执行完该同步方法之后;其他非同步方法及非静态的同步方法的执行不受影响
②同步代码块:synchronized放在对象前面限制一段代码的执行
同步条件:
缺点:
多个线程需要判断锁,较为消耗资源
同步线程例子(在main方法中,创建多个线程)
public static void main(String[] args) {
TicketRes tr = new TicketRes();
Thread t1 = new Thread(tr);
Thread t2 = new Thread(tr);
Thread t3 = new Thread(tr);
t1.start();
t2.start();
t3.start();
}
同步代码块的应用(单例模式中懒汉式是线程不安全的,同步可以解决不安全问题)
(相对于之前学过的"饿汉式单例")
二、锁的选择
锁的选择:
--多个线程共享资源,为了保证数据安全,需要同步
--一般情况下选择资源作为锁(必须为引用类型)即可,也可以选择其他唯一的对象
死锁:
死锁发生的条件:
--同步中嵌套同步
--锁不同
三、守护线程
在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)
--所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
--用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:
--thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
--在Daemon线程中产生的新线程也是Daemon的。
--守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
四、线程间通信
Java实现线程通信的方法
①wait()方法
挂起当前线程,并释放共享资源的锁
②notify()方法
在因调用该对象的wait()方法而阻塞的线程中随机选择一个解除阻塞,但要等到获得锁后才可真正执行
③notifyAll()方法
将因调用该对象的wait()方法而阻塞的所有线程一次性全部解除阻塞
package com.qf.demo2;
// 12A34B56C
/**
* 线程间通信 需要放在线程同步中
* 1 资源类中方法 是同步的
* 2 boolean标志 标志状态
* 3 方法里面 先判断状态(是否需要等待)
* 4 如果不需要等待 执行 打印 加减操作
* 5 执行完操作以后需要切换状态
* 6 唤醒对象线程
*
*/
public class Resource {
boolean flag = true; // true 打印了字母还没有打印数字 字母等着 数字打印
// false 打印了数字了 还没有打印字母 数字等着 字母打印
// 先打印两个数字
public synchronized void printNum(int num){
if(flag==false){// 打印了数字 还没打印字母
try {
this.wait();// 数字等着
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// true 打印了字母 还没打印数字 打印数字
System.out.print(num+""+(num+1));// 1 12 34
//已经打印完了数字了 切换状态
flag = false;
//唤醒字母线程
this.notify();
}
// 在打印一个字母
public synchronized void printLetter(int letter){
if(flag == true){//打印了字母还没有打印数字
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// false 打印了数字还没打印字母 打印字母
System.out.print((char)(letter+65));
// 打印完了字母了 ,需要切换状态
//切换成 打印完了字母,还没打印数字 true
flag = true;
// 唤醒对方线程
this.notify();
}
}
package com.qf.demo2;
public class Test {
public static void main(String[] args) {
Resource resource = new Resource();
Number number = new Number(resource);
Letter letter = new Letter(resource);
Thread thread = new Thread(number);
Thread thread2 = new Thread(letter);
thread.start();
thread2.start();
}
}
class Number implements Runnable{
Resource resource ;
public Number(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int i = 1; i < 52; i+=2) {
// 1 3 5 7 9
resource.printNum(i);
}
}
}
class Letter implements Runnable{
Resource resource ;
public Letter(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (int i = 0; i < 26; i++) {
resource.printLetter(i);
}
}
}
总结:
wait( ),notify( ),notifyAll( )都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( ),notify( ),notifyAll( ) 的功能,因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了。
IllegalMonitorStateException 异常
wait( )进行线程等待时,必须要取得这个锁对象的控制权(对象监视器),一般是放到synchronized(obj)代码中。
④在while循环里而不是if语句下使用wait,这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知
obj.wait( )释放了obj的锁,否则其他线程也无法获得obj的锁,也就无法在synchronized(obj){ obj.notify() } 代码段内唤醒A。
notify( )方法只会通知等待队列中的第一个相关线程(不会通知优先级比较高的线程)
notifyAll( )通知所有等待该竞争资源的线程(也不会按照线程的优先级来执行)
obj.wait( ),那么obj.notifyAll( )则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行