以下代码是Java中传递Baton程序的一部分:
主 P1(作家) P2(作家) P3(读者) P4(读者)
主();
package ReadersPreference;
import java.util.concurrent.Semaphore;
/**
* * @author me
*/
public class Main {
public static void main(String[] args) {
AnyData x = new AnyData(5.7);//gives writers something to write,
//readers something to read
Semaphore e = new Semaphore(1);//control entry
Semaphore r = new Semaphore(0);//used to delay readers
Semaphore w = new Semaphore(0);//used to delay writers
int nr = 0;//readers active
int nw = 0;//writers active
int dr = 0;//readers waiting
int dw = 0;//writers waiting
P1 r1 = new P1(e, r, w, x, nr, nw, dr, dw); // #reader thread 1
P2 r2 = new P2(e, r, w, x, nr, nw, dr, dw); // #reader thread 2
P5 r3 = new P5(e, r, w, x, nr, nw, dr, dw); // #reader thread 3
P6 r4 = new P6(e, r, w, x, nr, nw, dr, dw); // #reader thread 4
P3 w1 = new P3(e, r, w, x, nr, nw, dr, dw); // #writer thread 1
P4 w2 = new P4(e, r, w, x, nr, nw, dr, dw); // #writer thread 2
System.out.println("threads commanded to start");
r1.start(); // calls run() method in Thread
r2.start();
r3.start();
r4.start();
w1.start();
w2.start();
}//end of main
}//end of class
读者流程
package ReadersPreference;
import java.util.concurrent.Semaphore;
public class P1 extends Thread {
private Semaphore e;
private Semaphore r;
private Semaphore w;
private AnyData pillarbox;
private int nw;
private int nr;
private int dr;
private int dw;
public P1(Semaphore e, Semaphore r, Semaphore w, AnyData pbox,
int nw, int nr, int dr, int dw) {
this.nw = nw;
this.nr = nr;
this.dr = dr;
this.dw = dw;
this.e = e;
this.r = r;
this.w = w;
pillarbox = pbox;
}// end of constructor
public void run() {
PERFORM OPERATIONS
}// end of run method
}// end of class
现在,根据我的输出,它似乎工作。但是,我的讲师指出了两个主要缺陷。一个是计数器(nr,nw,dr,dw)是通过值而不是通过引用传递的。这意味着每个线程都会检查自己的数据副本,而不是使用共享变量,这会阻止程序按预期运行。
我一直在阅读很多关于通过价值和参考的文章,起初我觉得我的脑袋肿胀,我想我现在已经理解了大部分内容,但我仍然没有看到解决方案。我可以理解为什么信号量工作,因为它们已经是一个参考(相反,它们传递的值是一个参考)。我没有看到计数器的确切问题在哪里或者如何解决它。
当他说线程时,他是指引用类(构造函数/算法)还是主类中的实例化或者两者兼而有之?
基于对共享原语作为对象的阅读,最接近我提出的解决方案如下:
public void setCounters(int nr){nr = newNR;}
虽然我对如何实施它很模糊
第二个主要的缺陷是我创建了几个进程类文件并将每个文件作为一个线程运行,而不是编写两个(读取器/写入器)并根据需要使用尽可能多的对象。我是模糊的,这意味着什么,但是,考虑到我的输出打印语句对于每个进程都是唯一的,要在输出中唯一地标识每个进行验证,为什么这个方法在为某个目的完成时被认为是一个缺陷,它会影响读者/作者的解决方案吗?
答案 0 :(得分:2)
在我开始之前,好的变量名称总是比评论更好;坚持这个规则,你不能出错。想象一下,我在代码中的某个地方遇到了e
变量,我现在必须滚动到类的顶部并阅读注释以查看它是什么,然后返回到我所在的位置。这使代码几乎不可读......
您的第一个问题是您正在使用int
这是一种原始类型,这是通过值传递的。 @ Marcin的解决方案不是线程安全的;如果你做int++
这样的事情,那么当被多个线程调用时,这可以做各种奇怪的事情(例如没有递增,返回错误的值等)。 始终在多线程操作中使用线程安全对象。
正如@Marcin建议您可以将数据包装在一个类中以减少代码量:
public class SharedData<T> {
private final T data;
private final Semaphore entryControl = new Semaphore(1);
private final Semaphore readerDelay = new Semaphore(0);
private final Semaphore writerDelay = new Semaphore(0);
private final AtomicInteger activeReaders = new AtomicInteger(0);
private final AtomicInteger activeWriters = new AtomicInteger(0);
private final AtomicInteger waitingReaders = new AtomicInteger(0);
private final AtomicInteger waitingWriters = new AtomicInteger(0);
public SharedData(final T data) {
this.data = data;
}
//getters
}
我已将此作为通用类但您可以删除泛型并根据需要将“数据”设置为“对象” - 泛型更好,因为它提供了类型安全性,请阅读here。
该类使用AtomicInteger
对象,这是一个线程安全的整数,允许进行getAndSet(int newValue)
等原子操作 - 这样可以在访问单个值,但看起来你可能想要访问两个,这仍然不是线程安全的,所以你可能想要在数据类中添加一些方法:
public synchronized void makeReaderActive() {
//perform checks etc
waitingReaders.decrementAndGet();
activeReaders.incrementAndGet();
}
否则,读者可能会递减waitingReaders
,然后在递增之前,某些内容可以读取activeReaders
。
我认为,这会处理您的第一个查询;现在到了你的第二个。您的老师说您创建了几个流程类文件而不是创建实例。这是因为您复制粘贴了相同的文件,调用不同的东西(P1
,P2
等),这不是Java的工作原理。考虑一下您创建Semaphore
s:
Semaphore e = new Semaphore(1);//control entry
Semaphore r = new Semaphore(0);//used to delay readers
您有一个类文件(Semaphore.class),您已经创建了两个实例。您不必将JDK的Semaphore
类复制到另一个文件中并创建它。你不必这样做:
Semaphore1 e = new Semaphore1(1);//control entry
Semaphore2 r = new Semaphore2(0);//used to delay readers
因此;在您的示例中,我假设您有一些Reader
进程和一些Writer
进程,那么您需要两个类:
public class MyReader implements Callable<Void> {
private final String name;
private final SharedData sharedData;
public MyReader(final String name, final SharedData sharedData) {
this.name = name;
this.sharedData = sharedData;
}
@Override
public Void call() {
//do stuff
return null;
}
}
public class MyWriter implements Callable<Void> {
private final String name;
private final SharedData sharedData;
public MyWriter(final String name, final SharedData sharedData) {
this.name = name;
this.sharedData = sharedData;
}
@Override
public Void call() {
//do stuff
return null;
}
}
一个班级代表所有作家的作品,另一个班级代表所有读者的作品。我把它们变成Callable
而不是线程;这让我想到了下一点。
您不应该使用Thread
个对象,这些对象非常低,难以正确管理。您应该使用新的ExecutorService。因此,您的main
方法现在看起来像:
public static void main(String[] args) {
final SharedData<Double> sharedData = new SharedData<Double>(5.7);
final List<Callable<Void>> myCallables = new LinkedList<Callable<Void>>();
for (int i = 0; i < 4; ++i) {
myCallables.add(new MyReader("reader" + i, sharedData));
}
for (int i = 0; i < 2; ++i) {
myCallables.add(new MyWriter("writer" + i, sharedData));
}
final ExecutorService executorService = Executors.newFixedThreadPool(myCallables.size());
final List<Future<Void>> futures;
try {
futures = executorService.invokeAll(myCallables);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
for (final Future<Void> future : futures) {
try {
future.get();
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
} catch (ExecutionException ex) {
throw new RuntimeException(ex);
}
}
}
让我引导您完成此代码,以便您了解这里发生了什么。在第一行,我们创建了一个新的SharedData
类 - 在这种情况下,它是SharedData<Double>
;这意味着它包含的data
类型为Double
;这可能是任何事情。
接下来,我们创建一个List
Callable
,这是我们的工作类将要去的地方。然后我们将工作类放入循环中的List
;请注意,我们为每个MyReader
和MyWriter
创建了同一个类的实例 - 我们不需要为每个实例创建一个类文件。
然后我们创建一个新的ExecutorService
,其中一个线程池的大小与我们创建的工作类的数量相同 - 请注意我们可以拥有更少的线程。在这种情况下,每个线程都会分配一个工作类,然后当它们完成时,它们将被赋予一个新工作类,直到一切都完成。在我们的例子中,有足够的线程,因此每个简单的线程都可以分配一个工作类。
我们现在致电invokeAll
传递List
个工作类,这是我们向ExecutorService
call
提出所有Callable
的问题。这个方法一直阻塞,直到一切都完成,它可能会抛出InterrupedException
,就像任何等待的方法一样 - 在这种情况下,我们抛出异常并退出。
最后,这就是ExecutorService
闪耀的地方,我们遍历返回的Future
类列表并调用get
- 如果有ExecutionException
,这将抛出Callable
运行与未来相关的工作的任何问题。在这种情况下,我们会抛出异常。
请注意Void
的类型为Callable<Void>
(即它们被声明为Future
),然后过滤到Future<Void>
,因为它们属于Callable<MyData>
{1}}。如果您想从每个流程返回一些数据,那么您可以将类型更改为get
,然后Future
的{{1}}方法会返回此数据。
答案 1 :(得分:0)
考虑声明一个结构来保存变量,并传递结构而不是单个变量。这样,所有线程都可以处理相同的数据;即:看到彼此的变化。
示例:
static class Data {
int nr = 0;
int nw = 0;
int dr = 0;
int dw = 0;
}
然后将引用传递给Data
而不是原语nr,nw,dr,dw
示例:
Data d = new Data();
P1 r1 = new P1(e, r, w, x, d);
P2 r2 = new P1(e, r, w, x, d);
并且P1
的构造函数声明为:
public P1(Semaphore e, Semaphore r, Semaphore w, AnyData pbox, Data d)
确保您将共享Data
上的修改与synchronized (d) {...}
此外,也许没有必要复制类P1,P2,P3,P4
,考虑使用线程名称来区分它们以用于日志记录。
希望这有帮助。