一、通过实现Runnable接口创建线程
1. 定义实现Runnable接口的类
Runnable接口中有一个run()方法;用来定义线程运行体,定义自己的线程类实现Runnable接口并重写run()方法;
在测试类中创建线程类实例的时候将这个类的实例传递到线程实例内部,然后再启动;接下来举一个实现Runnable类的线程类来
看一下这个东西。
实现Runnable类:
package runnable;
public class MyThread implements Runnable{
public void run(){
System.out.println("进入子线程。。。");
for(int i=0;i<10;i++){
if(i%2 == 0){
System.out.println(i);
}
}
System.out.println("子线程执行结束。。。");
}
}
测试类:
package runnable;
public class MyThread implements Runnable{
public void run(){
System.out.println("进入子线程。。。");
for(int i=0;i<10;i++){
if(i%2 == 0){
System.out.println(i);
}
}
System.out.println("子线程执行结束。。。");
}
}
测试结果:
二、优势
1. 避免了java单继承的局限性;
2. 使用实现Runnable接口的方式创建线程时,可以为相同程序代码的多个线程提供共享的数据(资源共享)
三、多线程安全与同步问题
当run()方法体内的代码操作到了成员变量(共享数据)时,就可能会出现多线程安全问题(线程不同步问题)。
线程的同步
2. 关键字synchronized用来与对象的互斥锁关联。当某个对象用synchro修饰时,表明该对象在任一时刻只能由一个线程访问
(这个对象就变成了同步对象)。
3. 一个方法使用关键字synchroni修饰后,当一个线程A使用这个方法时,其他线程想使用这个方法就必须等待,直到线程A使用该方法(前提
是这些线程使用的是同一个同步对象)。
线程同步后效率会变低,变低的原因是:
1. 会丧失java多线程的并发优势,在执行到同步代码块(或同步方法时),只能有一个线程执行,其他线程必须等待执行同步代码块(或同步方法)
的线程释放同步对象的锁才能执行。
2. 其他等待所释放的线程会不断检查锁的状态,也浪费了一定的系统资源。
同步方法的同步对象:
1.对于非静态方法来说,this当前对象充当了同步对象
非静态方法的demo,卖火车票,每次只能卖一张票,访问时:
package synchronizeddemo;
public class TicketDemo implements Runnable{
private int ticket=5;
public void run() {
synchronized (this) {
for(int i=0;i<100;i++){
if(ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了一张票,剩余票数为:"+ticket);
}
}
}
}
}
线程的执行:
package synchronizeddemo;
public class Run {
public static void main(String[] args) {
TicketDemo td = new TicketDemo();
//加锁之后能够完成对共享变量的互斥访问
new Thread(td,"A线程").start();
new Thread(td,"B线程").start();
new Thread(td,"C线程").start();
}
}
测试结果为:2. 静态方法的同步对象成为"类对象",类对象代表的是这个类本身,所有通过类实例化的普通对象共享这个类对象,锁住的前提是多个线程共享一个对象。
package staticsync;
public class PersonRunnable implements Runnable{
private int money=1000;
public void run() {
makeMoney();
}
public static synchronized void makeMoney(){
try {
System.out.println(Thread.currentThread().getName()+"帮你赚钱了。。。");
Thread.sleep(1000);
//money+=1000;
System.out.println(Thread.currentThread().getName()+"帮你赚了"+"钱");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类:
package staticsync;
public class Run {
public static void main(String[] args) {
PersonRunnable pr1 = new PersonRunnable();
PersonRunnable pr2 = new PersonRunnable();
PersonRunnable pr3 = new PersonRunnable();
new Thread(pr1,"马云").start();
new Thread(pr2,"马化腾").start();
new Thread(pr3,"王健林").start();
}
}
测试结果为:注:同步代码块或者同步方法的区域成为临界区.
六、死锁问题(线程中的“假死”现象)
线程死锁的原因:
线程1锁住资源A等待资源B,线程2锁住资源B等待资源A,两个线程都在等待自己需要的资源,而这些资源被另外的线程锁住,这些线程你等我,我等你,谁也不愿意让出资源,这样死锁就产生了。
死锁中经典的哲学家进餐问题(哲学家进餐Demo):
package deathlock;
public class Knife {
public void knifeSay(){
System.out.println("我拿到刀子了。。。。");
}
}
package deathlock;
public class Fork {
public void forkSay(){
System.out.println("我拿到叉子了。。。");
}
}
实现一个线程类,见证一下死锁问题的发生:
package deathlock;
//模拟哲学家进餐问题,产生死锁,解决死锁是加重锁的力度
public class EatRunnable implements Runnable{
private boolean flag=false;
private static Knife knife = new Knife();
private static Fork fork = new Fork();
public void setFlag(boolean flag) {
this.flag = flag;
}
public void run(){
if(flag){
synchronized (knife) {
knife.knifeSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (fork) {
fork.forkSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完饭啦。。");
}else{
synchronized (fork) {
fork.forkSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (knife) {
knife.knifeSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完饭啦。。。");
}
}
}
测试类:
package deathlock;
public class Run {
public static void main(String[] args) {
EatRunnable er1 = new EatRunnable();
EatRunnable er2 = new EatRunnable();
er1.setFlag(true);
new Thread(er1,"康德").start();
new Thread(er2,"亚里士多德").start();
}
}
测试结果为:
从程序运行结果中可以看出,两个哲学家对象一个拿到了刀子,一个拿到了叉子,两个线程若想继续向下运行,还需要对方手里的“锁”,但这两个线程互不相让,导致程序无法向下运行,即发生了所谓的“死锁现象”。
解决办法还得从同步方法入手,在线程类中,由于“锁上加锁”就容易导致死锁问题的发生。因此,可以调整两个同步对象的顺序,使这两个哲学家获得“同步锁”的顺序是一样的,这样就可以使得在一方哲学家拿到锁之后,另一个在哲学家在执行时一样拿不到锁,就避免了“死锁”问题的发生;另外,这个问题发生的情况就是“锁里有锁”的情况,可以把锁里面的锁去除,放在外面,加大锁的力度即可。
七、Lock锁实现同步
1.Lock接口
2.ReentrantLock类(“可重入锁”)
package lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable{
private int ticket=5;
private ReentrantLock lock = new ReentrantLock();
public void run(){
for(int i=0;i<100;i++){
try {
lock.lock(); //获取锁,在需要同步的代码块上获取lock锁
if(ticket>0){
Thread.sleep(1000);
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了一张票,剩余票数为:"+ticket);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock(); // 执行完加锁的代码块后要将锁释放掉
}
}
}
}
写一个测试卖票类:
package lock;
public class Run {
public static void main(String[] args) {
TicketLock tl = new TicketLock();
new Thread(tl,"A窗口").start();
new Thread(tl,"B窗口").start();
new Thread(tl,"C窗口").start();
}
}
测试结果为:从测试结果可以看出:lock锁同样也可以实现同步的功能。
3.ReadWriterLock接口
4.ReentrantReadWriteLock类
5.接下来就写一个关于读写锁的demo
读取数据与修改数据,一个线程读取时可以有多个线程读取,但不允许写入数据;在写入数据时既不允许其他线程读取数据
也不允许其他线程读取数据:
package rwlock;
import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class InfoRunnable implements Runnable{
private static int data;
private boolean flag=false;
//创建读写锁对象,该属性对象可以获取读锁或写锁
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
public void setFlag(boolean flag) {
this.flag = flag;
}
public void run(){
if(flag){
for (int i = 0; i < 10; i++) {
writeData();
}
}else{
for (int i = 0; i < 10; i++) {
readData();
}
}
}
//写数据
public void writeData(){
try {
rw.writeLock().lock(); //获取写锁,若已经有线程占用了该写锁,则此时其他线程要求获得写锁的线程会一直等待释放写锁
System.out.println(Thread.currentThread().getName()+"准备写入数据...");
Thread.sleep(1000);
data = new Random().nextInt(10); //模拟写入数据
System.out.println(Thread.currentThread().getName()+"写数据完毕。。。。");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
rw.writeLock().unlock(); //执行完之后释放写锁啊。
}
}
//读取数据
public void readData(){
try {
rw.readLock().lock(); //获取读锁,若有一个线程占用了这个读锁,其他线程在申请写锁时不被允许,但仍允许申请读锁
System.out.println(Thread.currentThread().getName()+"准备读取数据");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"读取到的数据为:"+data);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
rw.readLock().unlock();
}
}
}
写一个测试类,多个线程读取数据,多个线程写入数据:package rwlock;
public class Run {
public static void main(String[] args) {
InfoRunnable ir1 = new InfoRunnable();
ir1.setFlag(true);
new Thread(ir1,"写入线程1").start();
new Thread(ir1,"写入线程2").start();
InfoRunnable ir2 = new InfoRunnable();
InfoRunnable ir3 = new InfoRunnable();
new Thread(ir2,"读取线程3").start();
new Thread(ir3,"读取线程4").start();
}
}
读取结果为:
从测试结果可以看出:在读取数据时多个线程可以同时读取到数据,而且读取的数据值肯定是相同的,但在读取数据时没有写数据进程的执行;在写入数据时仅有一条线程一口气执行完了写入数据的操作,没有读数据或写数据进程乱入。。
补充:
synchronized与Lock的区别和联系:
1.Lock是一个接口,而synchronized是java中的关键字,synchronized是内置的语言实现。
2.sychronized在发生异常时,会自动释放线程占有的锁,而Lock在发生异常时,若没有主动通过unlock()去释放锁则很可能造成死锁现象,因此使用lock时需要在finally块中去释放锁。
3.Lock可以提高多个线程进行读操作的效率诶。