目录
1. 线程安全问题
1.1 产生原因
- 不同的线程操作共享数据
- 操作共享数据的语句代码有多条
- 有以上两条,可能这个线程执行完判断语句还没有执行下面的语句的时候,切换到别的线程了,这样就会出现问题
1.2 解决办法
主要思想是,只要让某一代码块持有同一个对象(锁),那么多线程调用的时候就能保证这一块代码的原子性。这个锁对象,可以是多线程操作的同一个对象内部自己定义的,可以是这个多线程操作的这个对象本身,可以是这个对象对应的字节码文件对象
同步代码块
对于那些操作共享数据的代码,我们将其封装,可以加上synchronized(同步)
关键字标识的同步代码块。其实原理就是相当于加了个锁,当某个线程进入到这个语句中的时候,持有同步锁,如果执行到一半切换到其它线程的时候,其它线程拿不到锁,只能在外面干等着。
格式:
Object obj=new Object();
//run方法外部的同步锁,也必须公有
//run方法内部的同步代码块
synchronized(obj){
//需要同步的代码
}
同步函数
同步代码块和函数一样,都能封装代码,那让函数直接具有同步的功能不就行了吗。所以这里就到了同步函数,在函数定义的时候加个synchronozed
修饰符就行了
同步函数使用的锁是this,相当于synchronized(this){代码}
,所以同步函数与同步代码块相比更简单,但是功能也有了限制。
静态同步函数
java对字节码文件封装了对象,当类加载进内存的时候,堆内存里其实就有了对应的字节码文件对象。静态同步函数调用的时候,需要一个锁(锁必须是对象),虽然堆内存里没有对象,但是这个锁就是那个字节码文件对象。
相当于synchronized(this.getClass())
或者synchronized(Ticket.class)
1.3 单例设计模式的多线程问题
对于饿汉式,不存在多线程安全问题,但是对于懒汉式,因为要判断一下是否堆内存有对象那一步,所以就会出现安全问题。可以这样解决:
class Single{
private static Single s=null;
private Single(){}
public static Single getInstance(){
if(s==null){
synchronized(Single.class){
if(s==null)s=new Single();
}
}
return s;
}
}
注意这里如果直接把这个得到实例的静态函数声明为synchronized的话,每次获取对象都会判断锁而降低效率,而如果像上面一样定义,则会提高效率。
2. 线程间通信
之前的情况是多个线程运行相同的任务代码,处理相同的资源,现在的情况考虑多个线程处理同一个资源,但是任务代码却不同。
2.1 简单的示例代码
考虑下面这种情况,两个线程处理同一个资源,一个线程负责写入,一个线程负责读取。
- Resource:
public class Resource {
public String name;
public String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
- InputSource(负责写入数据):
public class InputSource implements Runnable{
private Resource r=null;
private int flag=0;
public InputSource(Resource r) {
super();
this.r = r;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
synchronized(this){
if(this.flag==0) {
r.setName("小明");
r.setSex("男");
}
else {
r.setName("小红");
r.setSex("女");
}
}
this.flag=(this.flag+1)%2;
}
}
}
- OutputSource(负责读取数据):
public class OutputSource implements Runnable {
private Resource r=null;
public OutputSource(Resource r) {
super();
this.r = r;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
synchronized(this) {
System.out.println(r.getName()+" : "+r.getSex());
}
}
}
}
- 主线程:
public class AccessResource {
public static void main(String[] args) {
// TODO Auto-generated method stub
Resource source=new Resource();
Thread inputit=new Thread(new InputSource(source));
Thread outputit=new Thread(new OutputSource(source));
inputit.start();
outputit.start();
}
}
对于这个资源,我们不断的写入小明 男
和小红 女
,然后不断的读取这两个数据,上述代码的同步锁是不同样的,所以对于数据的读写并没有得到有效的保护,所以可能会得到类似下面的结果:
小明 : 女
小明 : 女
小红 : 男
小红 : 女
小明 : 女
小明 : 女
当一个线程刚把名字改了,性别还没改的时候,另一个线程就直接把这个新名字和旧性别给输出了,这显然是不符合常理的。
这时候,我们可以将两个线程里的同步锁都设置为this.r,指向同一个资源对象,这时候就不会出现上述“人妖”的问题了
2.2 等待/唤醒机制
相关方法
wait()
:将本线程转换到睡眠状态,即交出CPU执行资格和执行权,存储到线程池(等待集)中
notify()
:将线程池中的线程随机唤醒一个
notifyAll()
:将线程池中的所有线程唤醒
- 它们继承自Object类,必须要由锁对象调用
- java中有多个线程,多个线程会有不同的同步代码块,而锁,便是把这些线程分组的一个东西。拥有相同的锁的同步代码块(不管你这个同步代码块相同不相同,属不属于同一个run方法)这些代码块所属的线程是必须保证里面的代码的原子性的,而上述的三种方法,操作的是这些线程,而不是全部的线程。
- 锁就像个监视器,监视线程,负责把关,上述方法也叫作监视器方法
- 不同的锁对应不同的线程池
- 对于一个同步代码块,不一定里面只有一个活着的线程,可以有多个活着的线程!比如使用wait()让多个线程等待,之后其它线程又使用notifyAll()把这几个线程同时复活,此时这几个线程就处于活着,并且在同一个同步代码块的状态!但是这并不意味着他们会同时执行,只有拿到锁的线程才会执行,拿不到的还是会继续等待。这个例子主要是明确一点,处于活着状态的线程不一定总是处在同步代码块之外。
改进后的示例代码
将姓名与性别交替赋值输出,改动代码如下:
- Resource:
public class Resource {
public String name="beginname";
public String sex="beginsex";
public boolean flag=false;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public synchronized void setMessage(String name,String sex) throws InterruptedException {
if(this.flag==false)this.wait();
this.setName(name);
this.setSex(sex);
this.flag=false;
this.notify();
}
public synchronized void printMsg() throws InterruptedException {
if(this.flag==true)this.wait();
System.out.println(this.getName()+" : "+this.getSex());
this.flag=true;
this.notify();
}
}
- InputResource:
public class InputSource implements Runnable{
private Resource r=null;
private int identityFlag=0;
public InputSource(Resource r) {
super();
this.r = r;
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
while(true) {
if(this.identityFlag==0)r.setMessage("小明", "男");
else r.setMessage("小红", "女");
this.identityFlag=(this.identityFlag+1)%2;
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- OutputResource:
public class OutputSource implements Runnable {
private Resource r=null;
public OutputSource(Resource r) {
super();
this.r = r;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
try {
r.printMsg();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
- 主线程:
public class AccessResource {
public static void main(String[] args) {
// TODO Auto-generated method stub
Resource resource=new Resource();
Thread t1=new Thread(new InputSource(resource));
Thread t2=new Thread(new OutputSource(resource));
t1.start();
t2.start();
}
}
wait和sleep区别
- wait:可以跟时间,可以不跟时间,释放CPU执行权,释放锁
- sleep:必须跟时间,释放CPU执行权,不释放锁
3. 线程调用的方法
停止线程
interrupt()
方法,可以将线程从冻结状态强制恢复到运行状态来,让线程具备CPU的执行资格,也就是说不管我是sleep还是wait,我都突然原地复活,由于这个方式太过暴力,所以每次调用这方法都会抛出InterruptException,当抛出这个异常的时候,可以考虑在catch里面设置停止线程的各种方法
守护线程
或者说后台线程,需要启动前声明
t1.setDaemon(true);
t1.start();
对于前台线程来说,某个线程的消失对另一个线程没任何关系,如果不手动结束,会一直按照代码执行。但是将某个线程声明为守护线程之后,前台线程结束后,该线程就会自动结束,无论冻结与否。
之所以叫做守护,是因为如果前台线程没了的话,我也没有守护的意义了,只好随前台而去
join
- 在主线程内部启动线程0后,如果再使用t.join(),那么主线程会冻结(可以interrupt),等待这个线程执行完之后再执行,在这之前没有CPU执行资格
- 在一个线程内部临时加入一个线程,使用join方法
setPriority
设置线程的优先级,最大为10,默认都是5